Là một lập trình viên, bạn nên làm quen với những kiến ​​thức cơ bản về thiết kế và phân tích thuật toán. Các thuật toán là một tập hợp các hướng dẫn được sử dụng để giải quyết một vấn đề cụ thể, và việc hiểu rõ các thuật toán khác nhau có thể nâng cao đáng kể khả năng giải quyết vấn đề của lập trình viên. Trong bài viết này, VietnamWorks inTECH sẽ giới thiệu 8 thuật toán hàng đầu mà mọi lập trình viên nên biết.

1. Thuật toán sắp xếp (Sorting algorithms)

Thuật toán sắp xếp (Sorting algorithms) được sử dụng để sắp xếp danh sách các mục theo một thứ tự cụ thể. Các thuật toán sắp xếp được sử dụng phổ biến nhất là sắp xếp nổi bọt (buble sort), sắp xếp chèn (insertion sort), sắp xếp chọn (selection sort), sắp xếp trộn (merge sort) và sắp xếp nhanh (quicksort). 

Mỗi thuật toán đều có những ưu điểm và nhược điểm riêng. Ví dụ, sắp xếp nổi bọt là một thuật toán đơn giản, nhưng nó không hiệu quả đối với các tập dữ liệu lớn. Ngược lại, sắp xếp trộn là một thuật toán hiệu quả, nhưng nó lại yêu cầu bộ nhớ lớn.

Ví dụ Sắp xếp nổi bọt:

 def binary_search(arr, l, r, x):

    if r >= l:

        mid = l + (r - l) // 2

        if arr[mid] == x:

            return mid

        elif arr[mid] > x:

            return binary_search(arr, l, mid-1, x)

        else:

            return binary_search(arr, mid+1, r, x)

    else:

        return -1

 

 arr = [2, 3, 4, 10, 40]

 x = 10

 result = binary_search(arr, 0, len(arr)-1, x)

 

 if result != -1:

    print("Element is present at index", str(result))

 else:

    print("Element is not present in array")

Output: Sorted array is: [11, 12, 22, 25, 34, 64, 90]

2. Thuật toán tìm kiếm (Search algorithms)

Các thuật toán tìm kiếm được sử dụng để tìm một mục cụ thể trong danh sách. Các thuật toán tìm kiếm được sử dụng phổ biến nhất là tìm kiếm tuyến tính (linear search)  và tìm kiếm nhị phân (binary search). 

Tìm kiếm tuyến tính là một thuật toán đơn giản, tìm kiếm từng mục trong danh sách cho đến khi tìm thấy mục mong muốn. Tìm kiếm nhị phân là một thuật toán hiệu quả hơn, chỉ tìm kiếm một phần cụ thể của danh sách dựa trên điểm giữa (midpoint).

Ví dụ tìm kiếm nhị phân:

from collections import defaultdict

 

class Graph:

 

    def __init__(self):

        self.graph = defaultdict(list)

 

    def add_edge(self, u, v):

        self.graph[u].append(v)

 

    def BFS(self, s):

        visited = [False] * (max(self.graph) + 1)

        queue = []

 

        queue.append(s)

        visited[s] = True

 

        while queue:

            s = queue.pop(0)

            print(s, end=" ")

 

            for i in self.graph[s]:

                if visited[i] == False:

                    queue.append(i)

                    visited[i] = True

 

g = Graph()

g.add_edge(0, 1)

g.add_edge(0, 2)

g.add_edge(1, 2)

g.add_edge(2, 0)

g.add_edge(2, 3)

g.add_edge(3, 3)

 

print("Following is Breadth First Traversal (starting from vertex 2)")

g.BFS(2)

Output: Element is present at index 3

3. Các thuật toán đồ thị (Graph algorithms)

Thuật toán đồ thị (Graph algorithms) được sử dụng để giải quyết các vấn đề liên quan đến đồ thị, là tập hợp các nút (nodes) và cạnh (edges).

Các thuật toán đồ thị được sử dụng phổ biến nhất là tìm kiếm theo chiều sâu (DFS) và tìm kiếm theo chiều rộng (BFS).

DFS được sử dụng để duyệt đồ thị, trong khi BFS được sử dụng để tìm đường đi ngắn nhất giữa hai nút.

Ví dụ về BFS:

 def fibonacci(n):

    if n <= 1:

        return n

    else:

        return fibonacci(n-1) + fibonacci(n-2)

 

 n = 10

 print("Fibonacci sequence:")

 for i in range(n):

    print(fibonacci(i), end=" ")

Output: Following is Breadth First Traversal (starting from vertex 2): 2 0 3 1

4. Quy hoạch động (Dynamic programming)

Quy hoạch động (Dynamic programming) là một kỹ thuật được sử dụng để giải các bài toán phức tạp bằng cách chia nhỏ chúng thành các bài toán con nhỏ hơn. Mỗi bài toán con chỉ được giải một lần và kết quả được lưu trong bộ nhớ để sử dụng trong tương lai. Các thuật toán quy hoạch động được sử dụng phổ biến nhất là dãy Fibonacci (Fibonacci sequence) và bài toán xếp ba lô (Knapsack).

Ví dụ dãy Fibonacci:

def fractional_knapsack(wt, val, capacity):

    n = len(wt)

    items = []

 

    for i in range(n):

        items.append((wt[i], val[i], i))

 

    items.sort(key=lambda x: x[1]/x[0], reverse=True)

 

    total_value = 0

    fractions = [0]*n

 

    for i in range(n):

    if items[i][0] <= capacity:

        fractions[items[i][2]] = 1

        total_value += items[i][1]

        capacity -= items[i][0]

    else:

        fractions[items[i][2]] = capacity / items[i][0]

        total_value += items[i][1] * (capacity / items[i][0])

        break

 

 return total_value, fractions

Output: 0 1 1 2 3 5 8 13 21 34

5. Thuật toán tham lam (Greedy algorithms)

Các thuật toán tham lam (Greedy algorithms) được sử dụng để giải quyết các vấn đề tối ưu hóa bằng cách đưa ra lựa chọn tốt nhất có thể ở mỗi bước. 

Các thuật toán tham lam được sử dụng phổ biến nhất là bài toán chọn hoạt động (activity selection problem) và thuật toán nén Huffman Coding.

Ví dụ về Fractional Knapsack:

def merge_sort(arr):

    if len(arr) > 1:

        mid = len(arr)//2

        left_half = arr[:mid]

        right_half = arr[mid:]

 

        merge_sort(left_half)

        merge_sort(right_half)

 

        i = j = k = 0

 

        while i < len(left_half) and j < len(right_half):

            if left_half[i] < right_half[j]:

                arr[k] = left_half[i]

                i += 1

            else:

                arr[k] = right_half[j]

                j += 1

            k += 1

 

        while i < len(left_half):

            arr[k] = left_half[i]

            i += 1

            k += 1

 

        while j < len(right_half):

            arr[k] = right_half[j]

            j += 1

            k += 1

 

arr = [64, 34, 25, 12, 22, 11, 90]

merge_sort(arr)

print("Sorted array is:", arr)

wt = [10, 20, 30]

val = [60, 100, 120]

capacity = 50

result = fractional_knapsack(wt, val, capacity)

print("Maximum value in Knapsack =", result[0])

print("Fractions taken =", result[1])

Output: Maximum value in Knapsack = 240.0 Fractions taken = [1, 1, 0.6666666666666666]

6. Thuật toán Chia để trị (Divide and Conquer)

Chia để trị (Divide and Conquer) là một kỹ thuật được sử dụng để giải các bài toán phức tạp bằng cách chia nhỏ chúng thành các bài toán con nhỏ hơn. Mỗi bài toán con được giải độc lập và các kết quả được kết hợp để giải bài toán ban đầu.

Các thuật toán Chia để trị được sử dụng phổ biến nhất là sắp xếp trộn (merge sort), sắp xếp nhanh (quicksort) và thuật toán Karatsuba.

Ví dụ về sắp xếp trộn:

def merge_sort(arr):

    if len(arr) > 1:

        mid = len(arr)//2

        left_half = arr[:mid]

        right_half = arr[mid:]

 

        merge_sort(left_half)

        merge_sort(right_half)

 

        i = j = k = 0

 

        while i < len(left_half) and j < len(right_half):

            if left_half[i] < right_half[j]:

                arr[k] = left_half[i]

                i += 1

            else:

                arr[k] = right_half[j]

                j += 1

            k += 1

 

        while i < len(left_half):

            arr[k] = left_half[i]

            i += 1

            k += 1

 

        while j < len(right_half):

            arr[k] = right_half[j]

            j += 1

            k += 1

 

arr = [64, 34, 25, 12, 22, 11, 90]

merge_sort(arr)

print("Sorted array is:", arr)

Output: Sorted array is: [11, 12, 22, 25, 34, 64, 90]

7. Thuật toán quay lui (Backtracking)

Quay lui (Backtracking) là một kỹ thuật được sử dụng để giải quyết vấn đề bằng cách khám phá tất cả các giải pháp có thể. Nếu một giải pháp được tìm thấy, nó sẽ được chấp nhận và nếu không, thuật toán sẽ quay lại và tìm một giải pháp khác. 

Các thuật toán quay lui được sử dụng phổ biến nhất là bài toán tám quân hậu (eight queens problem) và bài toán người bán hàng (the travelling salesman problem).

Ví dụ N-Queens:

def is_safe(board, row, col, n):

    for i in range(col):

        if board[row][i] == 1:

            return False

 

    for i, j in zip(range(row, -1, -1), range(col, -1, -1)):

        if board[i][j] == 1:

            return False

 

    for i, j in zip(range(row, n, 1), range(col, -1, -1)):

        if board[i][j] == 1:

            return False

 

    return True

 

def solve_n_queens(board, col, n):

    if col == n:

        for i in range(n):

            for j in range(n):

                print(board[i][j], end=" ")

            print()

        print()

        return True

 

    res = False

    for i in range(n):

        if is_safe(board, i, col, n):

            board[i][col] = 1

            res = solve_n_queens(board, col+1, n) or res

            board[i][col] = 0

 

    return res

 

n = 4

board = [[0 for x in range(n)] for y in range(n)]

solve_n_queens(board, 0, n)

Output:

0 0 1 0

1 0 0 0

0 0 0 1

0 1 0 0

 

0 1 0 0

0 0 0 1

1 0 0 0

0 0 1 0

8. Thuật toán ngẫu nhiên (Randomised Algorithm)

Các thuật toán ngẫu nhiên (Randomised Algorithm) sử dụng một trình tạo số ngẫu nhiên để đưa ra quyết định trong quá trình thực hiện thuật toán. Các thuật toán này được sử dụng để giải quyết các vấn đề khó một cách xác định. 

Các thuật toán ngẫu nhiên được sử dụng phổ biến nhất là thuật toán sắp xếp nhanh (quicksort) và thuật toán Monte Carlo.

Ví dụ về thuật toán nhanh

import random

 

def partition(arr, low, high):

    i = (low-1)

    pivot = arr[high]

 

    for j in range(low, high):

        if arr[j] <= pivot:

            i = i+1

            arr[i], arr[j] = arr[j], arr[i]

 

    arr[i+1], arr[high] = arr[high], arr[i+1]

    return (i+1)

 

def quick_sort(arr, low, high):

    if low < high:

        # Randomly select a pivot

        pivot_index = random.randint(low, high)

        arr[pivot_index], arr[high] = arr[high], arr[pivot_index]

 

        pi = partition(arr, low, high)

 

        quick_sort(arr, low, pi-1)

        quick_sort(arr, pi+1, high)

 

arr = [10, 7, 8, 9, 1, 5]

n = len(arr)

quick_sort(arr, 0, n-1)

print("Sorted array is:")

for i in range(n):

    print(arr[i], end=" ")

Output: Sorted array is: 1 5 7 8 9 10

Lời kết

Việc hiểu 8 thuật toán này có thể nâng cao đáng kể khả năng giải quyết vấn đề của lập trình viên. Mỗi thuật toán đều có những ưu điểm và nhược điểm riêng và việc chọn thuật toán phù hợp cho một vấn đề cụ thể là rất quan trọng để đạt được kết quả tối ưu. Bằng cách tự làm quen với các thuật toán này, các lập trình viên có thể cải thiện kỹ năng viết code của mình và trở thành những người giải quyết vấn đề hiệu quả hơn.

VietnamWorks inTECH