Processing math: 0%

CÁC THUẬT TOÁN SẮP XẾP CƠ BẢN VỚI PYTHON

Bài toán: Cho một mảng A có n phần tử hãy sắp xếp mảng theo thứ tự không giảm.

1. Sắp xếp chọn.

Thuật toán này khá đơn giản:

  • Bước 1: Chọn phần tử nhỏ nhất trong tập hợp cần sắp xếp
  • Bước 2: Hoán đổi vị trí của phần tử nhỏ nhất vừa tìm được trong tập hợp với phần tử ngoài cùng bên trái
  • Bước 3: Loại phần tử ngoài cùng bên trái ra khỏi tập hợp cần sắp xếp
  • Bước 4: Lặp lại quá trình trên.
Code python
def SelectionSort(A):
	n = len(A)
	for i in range(n):
		minIndex=i
		for j in range(i+1, n):
			if A[j]

Độ phức tạp của thuật toán là O(n^2) 

2. Sắp xếp trộn. (Merge Sort)

Sắp xếp trộn sử dụng kỹ thuật chia để trị

  • Bước 1: Chia đầu vào thành 2 nửa.
  • Bước 2: Xắp sếp 2 nửa bằng cách đệ quy. quá trình đệ quy kết thúc khi độ dài mảng bằng 1.
  • Bước 3: Trộn hai nửa đã xắp sếp lại thành một mảng không giảm.
Pseudocode
MergeSort(A[1...n]):
if \text{ } n = 1:
    return \text{ } A
mid \text{ } \leftarrow \left \lfloor \frac{n}{2} \right \rfloor
B \text{ } \leftarrow \text{ } MergeSort(A[1...mid])
C \text{ } \leftarrow \text{ } MergeSort(A[nid...n])
A' \text{ } \leftarrow \text{ } Merge(B,C)
return \text{ } A'
Merge(B[1...p],C[1...q]):
D \text{ } \leftarrow mảng trống có kích thước p+q
Điều kiện B và C khác rỗng:
    b \text{ } \leftarrow phần tử đầu trong B

    c \text{ } \leftarrow phần tử đầu trong C
    if\text{ } b \leq c:
        chuyển b từ B sang D
    else:
        chuyển c từ C sang D
chuyển phần còn lại của B và C sang D
return D

Code python

def MergeSort(A: list) -> list:
  def Merge(B: list, C: list) -> list:
    while B and C:
      yield (B if B[0]<=C[0] else C).pop(0)
    yield from B
    yield from C
  if len(A) == 1:
    return A
  mid = len(A)//2
  B = MergeSort(A[: mid])
  C = MergeSort(A[mid: ])
  result = list(Merge(B, C))
  return result

Code có sử dụng while loops and lists và yield trong python

Độ phức tạp về thời gian của hàm Merge là O(n)

Như vậy ta có độ phức tạp của thuật toán là T(n) \leq 2T(\frac{n}{2})+O(n) 

Áp dụng định lý master ta có T(n)=O(n\log_2n) 

3. Sắp xếp đếm

Giả sử đầu vào là một mảng A[1...n] và tất cả các phần tử là số nguyên giới hạn trong khoảng từ a đến b.

Đúng với tên gọi 'sắp xếm đếm' ta đếm số phần tử trong mảng A sau đó sắp xếp chúng. 

Pseudocode

CountSort(A[1...n]):
if \text{ } A = []:
    return \text{ } A
Count[min(A)...max(A)] \text{ }\leftarrow [0,...,0]
\text{for i from 1 to n:}
    Count[A[i]] \leftarrow Count[A[i]] + 1
\text{for i from 2 to max(A) - min(A) +1:}
    Count[i] \leftarrow Count[i-1] + Count[i]
Result[1...n] \leftarrow [0...0]
\text{for i from 1 to n:}
    Result[Count[A[i]-min(A)]] \leftarrow A[i]
    Count[A[i]-min(A)] \leftarrow Count[A[i]-min(A)]-1

\text{return Result}

Code python

def CountSort(A: list) -> list:
	if A == []:
		return A
	A_len = len(A)
	A_min = min(A)
	A_max = max(A)
	counting_A_length = A_max - A_min + 1
	counting_A = [0]*counting_A_length
	for i in A:
		counting_A[i-A_min] += 1
	for i in range(1,counting_A_length):
		counting_A[i] += counting_A[i-1]
	result = [0]*A_len
	for i in range(A_len):
		result[counting_A[A[i]-A_min] - 1] = A[i]
		counting_A[A[i]-A_min] -= 1
	return result
A=[-2,-2,-3,-1]
print(CountSort(A))

Độ phức tạp của thuật toán là O(n+b-a)

4. Sắp xếp nhanh. (Quick Sort)

Sắp xếp nhanh cũng sử dụng giải thuật chia để trị

  • Bước 1: Chọn phần tử đầu tiên của mảng A làm pivot. Chia mảng thành 2 nửa, nửa bên trái có phần tử nhỏ hơn hoặc bằng A(l), nửa bên phải ngược lại.  
  • Bước 2: Tiếp tục chia nhỏ các bài toán con bằng đệ quy. Quá trình đệ quy kết thúc khi bài toán con có độ dài bằng 1.
  • Bước 3: Ta được mảng đã sắp xếp.
Pseudocode
QuickSort(A, l, r):
if \text{ } l \geq r:
    return
m \leftarrow partition(A, l, r)
QuickSort(A, l, m-1)
QuickSort(A, m+1, r)
source coursera by hauvi
Partition(A, l, r):
pivot \leftarrow A[l]
j=l
\text{for i from l+1 to r:}
    if \text{ } A[i] \leq A[l]:
        j \leftarrow j+1
        swap \text{ } A[j] \text{ and } A[i]
swap \text{ } A[l] \text{ and } A[j]
return \text{ } j

Code python

def QuickSort(A, left, right):
    def partition(A, l, r):
        j = l
        x = A[l]
        for i in range(l+1,r+1):
            if A[i] <= x:
                j = j + 1
                A[i], A[j] = A[j], A[i]
        A[j], A[l] = A[l], A[j]
        return j
    if left >= right:
        return 
    m = partition(A, left, right)
    QuickSort(A, left, m-1)
    QuickSort(A, m+1, right)
A = [6,2,8,9,5]
left, right = 0, len(A) - 1
QuickSort(A, left, right)
print(A)

Độ phức tạp của thuật toán

Thời gian chạy của Partition(A, l, r)O(n). Vậy thời gian chạy của QuickSort(A, l, r)T(n)=T(m-1)+T(n-m)+O(n)

Trường hợp tệ nhất T(n)=O(n^2). Trường hợp tốt nhất T(n)=O(n\log_2n)

Nếu như ta luôn chọn phân tử đầu tiên của mảng A làm pivot thì rất có thể xảy ra trường hợp tồi tệ là O(n^2) theo như  định luật Murphy. Do đó ta cần chọn pivot ngẫu nhiên để luôn đảm bảo xác suất xảy ra trường hợp tồi tệ là thấp và xác suất xảy ra trường hợp tốt O(n\log_2n) cao.

Pseudocode RandomizedQuickSort

RandomizedQuickSort(A, left, right):
if \text{ } left \geq right:
    return
k \leftarrow \text{random number between left and right}
swap \text{ } A[l] \text{ } and \text{ } A[k]
m \leftarrow partition(A, l, r)
RandomizedQuickSort(A, l, m-1)
RandomizedQuickSort(A, m+1, r)

Code python

import random
def RandomizedQuickSort(A, left, right):
  def Partition(A, l, r):
    pivot = A[l]
    j = l
    for i in range(l+1, r+1):
      if A[i] <= pivot:
        j += 1
        A[j], A[i] = A[i], A[j]
    A[l], A[j] = A[j], A[l]
    return j
  if left >= right:
    return 
  k = random.randint(left, right)
  A[left], A[k] = A[k], A[left]
  m = Partition(A, left, right)
  RandomizedQuickSort(A, left, m - 1)
  RandomizedQuickSort(A, m + 1, right)
A = [5,1,8,9,2,4,7,3,6]
left , right = 0, len(A) - 1 
RandomizedQuickSort(A, left, right)
print(A)

Định lý

Giả sử rằng tất cả các phần tử của A[1...n] đôi một khác nhau. Khi đó thời gian chạy trung bình của RandomizedQuickSort(A) là O(n\log_2n) trong khi trường hợp xấu nhất thời gian chạy của thuật toán là O(n^2).

Chứng minh định lý

  • Nhận xét: Thời gian chạy của thuật toán tỉ lệ thuận với số phép so sánh trong hàm partition(A)
Để thuận tiện trong việc phân tích thời gian chạy ta gọi mảng A'[1...n] là mảng đã được sắp xếp.
  • Với i<j, ta định nghĩa biến ngẫu nhiên \chi_{ij} như sau
\chi_{ij}=\begin{cases} 1& A'[i] \text{ and } A'[j] \text{ được so sánh} \\ 0& \text{ngược lại} \\ \end{cases}

  • Với mọi i < j,A[i]A[j] được so sánh chính xác một lần hoặc hoàn toàn không được so sánh
  • Thời gian chạy trong trường hợp xấu nhất là O(n^2). Tuy nhiên xác suất rất thấp.
  • Nhận xét quan trọng: \chi_{ij}=1 nếu pivot được chọn đầu tiên trong A'[i...j]A[i] hoặc A[j]
  • Khi đó Prob(\chi_{ij}=1)=\frac{2}{j-i+1}
Gọi X là số lượng phép so sánh trong quá trình chạy hàm RandomizedQuickSort(A).
Ta có X= \sum_{i=1}^{n}\sum_{j=i+1}^n\chi_{ij}
Luật số lớn mạnh nói rằng trung bình cộng của X hội tụ hầu như chắc chắn về E(X) 
Phát biểu này cho phép ta ước tính thời gian chạy trung bình của thuật toán là
\begin{align*}E\left(X\right)&=E\left(\sum_{i=1}^{n}\sum_{j=i+1}^n\chi_{ij}\right)=\sum_{i=1}^{n}\sum_{j=i+1}^nE(\chi_{ij}) = \sum_{i=1}^{n}\sum_{j=i+1}^n\left(Prob(\chi_{ij}=1).1+Prob(\chi_{ij}=0).0\right)\\&=\sum_{i=1}^{n}\sum_{j=i+1}^nProb(\chi_{ij}=1)=\sum_{i=1}^{n}\sum_{j=i+1}^n\frac{2}{j-i+1}=\sum_{i=1}^n2\left(\frac{1}{2}+\frac{1}{3}+\dots+\frac{1}{n-i+1} \right).\end{align*}
Tổng 1+\frac{1}{2}+\dots+\frac{1}{n} ký hiệu là H_n, được gọi là chuỗi điều hòa thứ n và nằm trong khoảng [\ln n,1+\ln n]. Vì vậy 
E(X)<2n(H_n-1)\leq 2n\ln n=\Theta(n\ln n).
Định lý  trên đã được chứng minh, để ý thấy rằng nếu các phần tử của mảng A[1...n] không thỏa mãn điều kiện đôi một khác nhau thì các phân tích ở trên sẽ không đúng, vì vậy để xử lý các phần tử bằng nhau, chúng ta thay thế dòng 
m \leftarrow Partition(A, l, r)
bằng dòng 
(m_1,m_2) \leftarrow Partition3(A, l, r)

sao cho
  • Với mọi l \leq k \leq m_1-1,A[k]  < X
    • Với mọi m_1 \leq k \leq m_2, A[k] = X
    • Với mọi m_1+1 \leq k \leq r, A[k]>X
    source coursera by hauvi

    Pseudocode RandomizedQuickSort
    RandomizedQuickSort(A, left, right):
    if \text{ } l \geq r:
        return
    k \leftarrow \text{ramdom number between l and r}
    swap \text{ } A[l] \text{ }and \text{ }A[k]
    (m_1,m_2) \leftarrow Partition3(A, left, right)
    RandomizedQuickSort(A, left, m_1-1)
    RandomizedQuickSort(A, m_2+1, right)
    Partition3(A, l, r):
    pivot \leftarrow A[l] 
    m_1, m_2, i \leftarrow l, r, l
    while \text{ } i \leq m_2:
        if \text{ } A[i] < pivot:
            swap \text{ } A[i] \text{ and } A[m_1]
            i,m_1 \leftarrow i+1,m_1+1
        elif \text{ } A[i] = pivot:
            i \leftarrow i+1
        elif \text{ } A[i] > pivot:
            swap \text{ } A[i] \text{ and } A[m_2]
            m_2 \leftarrow m_2 -1
        return m_1,m_2

    Code python

    def Partition3(A, l, r):
      pivot = A[l]
      m_1, m_2 = l, r
      i = l
      while i <= m_2:
        if A[i] < pivot:
          A[i], A[m_1] = A[m_1], A[i]
          m_1 += 1
          i += 1
        elif A[i] == pivot:
          i += 1
        else:
          A[i], A[m_2] = A[m_2], A[i]
          m_2 -= 1
      return m_1, m_2
    

    Bài viết khá dài, cho nên mình xin dừng lại tại đây, qua tết sẽ viết bổ sung phần rút gọn code đệ quy của quick sort, và một số chú thích thêm, bài viết này tham khảo rất nhiều từ giaithuatlaptrinh.com

    Không có nhận xét nào:

    Đăng nhận xét

    Bài 6: NGĂN XẾP VÀ HÀNG ĐỢI

     1. Ngăn xếp. Định nghĩa Ngăn xếp: Là một loại dữ liệu trừu tượng và các thao tác có thể dùng: Push(data): Thêm data vào ngăn xếp Top(): Tìm...