Chuyển đến nội dung chính

Các thuật toán sắp xếp

Với tất cả các loại sort, thì mình sẽ sử dụng hàm swap (hoán vị) để thuận tiện cho việc thao tác nhé!
Mã nguồn:
1
2
3
4
5
6
void swap(int&a, int&b)
{
  int temp = a;
  a = b;
  b = temp;
}
Và mình quy ước: a là tên mảng, n là số phần tử 😀

1. Bubble sort

Bubble sort (hay còn gọi là Sắp xếp nổi bọt) là 1 thuật toán dễ cài đặt nhất trong tất cả các loại thuật toán, vì vậy nó được mấy bạn học sinh, sinh viên ưa xài (mình lâu lâu làm biếng cũng hay xài :v). Như tên gọi, thuật toán này sẽ tìm vị trí được xem là nhỏ nhất trong dãy bằng cách bắt cặp so sánh, rồi cho nó “trồi” lên vị trí đầu tiên, tiếp tục tới những phần tử tiếp theo, nó cứ trồi lên từ bé đến lớn, giống như những bong bóng nổi bọt vậy.
bubble-sort
Mã nguồn:
1
2
3
4
5
6
7
8
9
10
void BubbleSort(int a[], int n)
{
  for (int i = 0; i < n - 1; i++)
  {
    for (int j = n - 1; j > i; j--)
    {
      if (a[j] < a[j - 1]) swap(a[j], a[j - 1]);
    }
  }
}
Vì đây là thuật toán dễ cài đặt, sử dụng nên nó cũng khá là chậm. Với trường hợp xấu nhất có thể xảy ra, độ phức tạp của thuật toán này là O(n2), chậm nhất

2. Insertion sort

Insertion sort (hay còn gọi là Sắp xếp chèn) là 1 thuật toán khá đơn giản bằng cách chọn những phần tử nhỏ nhất để đưa lên đầu trong 1 dãy. Giống như ta đánh bài “tiến lên miền Nam” (mình không có tuyên truyền cờ bạc nha :v), chọn 13 lá. Duyệt từ trái qua phải, thấy lá nào nhỏ hơn 1 dãy lá bài bên trái thì bốc ra và nhét vào đúng vị trí sao cho dãy đã sắp xếp luôn tăng/giảm dần.
Insertion sort cũng vậy, ban đầu ta có 1 vị trí gọi là x. Dãy các phần tử từ 0 -> (x – 1) là dãy đã sắp xếp, còn dãy từ x -> n là dãy chưa được sắp xếp. Nhiệm vụ của ta là duyệt dãy x -> n, thấy phần tử nào thì tìm vị trí phù hợp trong dãy 0 -> (x – 1) mà nhét vào.
insertion-sort
Mã nguồn:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void InsertionSort(int a[], int n)
{
  int x, temp;
  for (int i = 1; i < n; i++)
  {
    x = i - 1; // Bắt đầu tìm x
    temp = a[i]; // Duyệt ngược từ phải qua, tìm vị trí phù hợp để chèn
    while (x >= 0 && a[x] > temp)
    {
      a[x + 1] = a[x];
      x--;
    }
    a[x + 1] = temp; // Gán vào vị trí x đã tìm
  }
}
Insertion sort trong trường hợp tốt nhất (tất cả các phần tử đã được sắp xếp sẵn) thì độ phức tạp của thuật toán là O(n) (vì chỉ cần duyệt n lần từ trái qua, không có chèn). Tuy nhiên, với trường hợp xấu nhất thì độ phức tạp có thể lên tới O(n2).

3. Selection sort

Selection sort (hay còn gọi là Sắp xếp chọn), là 1 trong những thuật toán dễ cài đặt và học nhất. Đúng như tên gọi, ta chỉ cần duyệt hết mảng, thấy thằng nào nhỏ nhất thì nắm đầu nó vứt lên đầu bảng.
Để làm được điều này, ta cần có 1 biến x tương tự như Insertion sort. Ta duyệt từ x -> n, nếu tìm được phần tử nhỏ nhất thì ta hoán vị nó với vị trí x.
selection-sort
Mã nguồn:
1
2
3
4
5
6
7
8
9
void SelectSort(int a[], int n)
{
  for (int i = 0; i < n; i++)
  {
    int x = i;
    for (int j = x; j < n; j++) if (a[x] > a[j]) x = j;
    swap(a[i], a[x]);
  }
}
Thuật toán này mặc dù có ưu việt là ít đổi chỗ các phần tử nhất, tuy nhiên vì ta phải tìm x, nên độ phức tạp của thuật toán cũng tương đương là O(n2).

4. Merge sort

Merge sort (hay còn gọi là Sắp xếp trộn) là 1 thuật toán điển hình của lối thuật toán Chia để trị(Divide and Conquer, D&C). Về ý tưởng thì ta chia 1 mảng lớn thành 2 mảng con, đến 1 điều kiện cụ thể (thường là chỉ còn 2 phần tử) rồi so sánh, sau đó gom từng mảng con lại thành mảng lớn để ra kết quả.
Giả sử ta có 2 mảng đã được sắp xếp sẵn là A = { 1, 3, 6, 9 } và B = { 2, 4, 5 }, ta muốn trộn 2 mảng đó thành 1 mảng lớn là AB thì ta cần phải so sánh 2 vị trí đầu tiên, nếu vị trí nào bé/lớn hơn thì ta cho vào mảng trộn:
  • A = { 1, 3, 6, 9 }, B = { 2, 4, 5 }, 1 < 2 => AB = { 1 }
  • A = { 3, 6, 9 }, B = { 2, 4, 5 }, 3 > 2 => AB = { 1, 2 }
  • A = { 3, 6, 9 }, B = { 4, 5 }, 3 < 4 => AB = { 1, 2, 3 }
  • A = { 6, 9 }, B = { 4, 5 }, 6 > 4 => AB = { 1, 2, 3, 4 }
  • A = { 6, 9 }, B = { 5 }, 6 > 5 => AB = { 1, 2, 3, 4, 5 }
  • A = { 6, 9 }, B = { }, B rỗng => nhét toàn bộ mảng A vào AB = { 1, 2, 3, 4, 5, 6, 9 }
merge-sort
Mã nguồn:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void Merge(int a[], int left, int mid, int right)
{
  // Tạo 2 vị trí mảng con left -> mid và mid + 1 -> right
  int left1 = left, right1 = mid, left2 = mid + 1, right2 = right;
  int index = left;
  int *b = new int[right - left + 1];
   
  while (left1 <= right1 && left2 <= right2)
  {
    if (a[left1] < a[left2]) // So sánh 2 vị trí đầu trong 2 mảng con
    {
      b[index] = a[left1]; // Bắt đầu trộn
      index++; left1++; // Xê dịch vị trí để tiếp tục so sánh
    }
    else
    {
      // Tương tự trên
      b[index] = a[left2];
      index++; left2++;
    }
  }
  // Trường hợp 1 trong 2 mảng đã hết phần tử
  if (left2 > right2)
  {
    while (left1 <= right1)
    {
      b[index] = a[left1];
      index++; right1++;
    }
  }
  if (left1 > right1)
  {
    while (left2 <= right2)
    {
      b[index] = a[left2];
      index++;
      left2++;
    }
  }
  // Cuối cùng, ta gán mảng đã trộn lại vào mảng gốc
  for (index = left; index <= right; index++)
  {
    a[index] = b[index];
  }
}
void MergeSort(int a[], int left, int right)
{
  if (left < right)
  {
    int mid = (left + right) / 2;
    // Bắt đầu chia mảng lớn thành 2 mảng con
    MergeSort(a, left, mid);
    MergeSort(a, mid + 1, right);
    // Chia xong thì trộn thôi :3
    Merge(a, left, mid, right);
  }
}
Tất nhiên, Merge sort có ưu điểm là nhanh hơn hẳn 3 loại sort phía trên, độ phức tạp của thuật toán O(n*log(n)). Tuy nhiên bạn phải hiểu rõ cơ chế của thuật toán, cộng với việc cài đặt tương đối khó.

5. Heap sort

Heap sort (hay còn gọi là Sắp xếp vun đống) là dạng sắp xếp dựa trên cây nhị phân. Cây nhị phân được xác định như sau: tại nút i có nút con trái là 2*(i + 1) – 1 và nút con phải là 2*(i + 1).


Hình minh hoạ cách biểu diễn cây nhị phân của mảng
Hình minh hoạ cách biểu diễn cây nhị phân của mảng

Ta xây dựng heap sao cho nút cha đều lớn hơn 2 nút con. Khi đó nút gốc có giá trị lớn nhất. Sau đó ta hoán vị nút gốc với nút thứ n – 1 và xây dựng lại heap mới từ 0 -> (n – 2) và nút cha lớn hơn 2 nút con. Lặp đi lặp lại việc xây dựng và hoán vị nút gốc với nút cuối trong heap, ta sẽ được mảng đã sắp xếp.
heap-sort
Mã nguồn:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Ta xem i là nút cha
void MaxHeapify(int a[], int n, int i)
{
  int left = 2*(i + 1) - 1;
  int right = 2*(i + 1);
  int max;
  // Tìm nút cha lớn nhất
  if (left < n && a[left] > a[i]) max = left;
  else max = i;
  if (right < n && a[right] > a[max]) max = right;
   
  if (i != max)
  {
    swap(a[i], a[max]);
    MaxHeapify(a, n, max); // Đệ quy với nút cha lần này là max
  }
}
void BuildHeap(int a[], int n)
{
  // Xây dựng heap chỉ lặp nửa so với dãy mảng
  // Do ta cần xây dựng cây nhị phân nên cần phải đi từ nhánh về gốc
  // Vì vậy mảng cần phải duyệt từ giữa (nút con) về đầu (nút gốc)
  for (int i = n / 2 - 1; i >= 0; i--)
  {
    Heapify(a, n, i);
  }
}
void HeapSort(int a[], int n)
{
  BuildHeap(a, n);
  // Lúc này a[0] là phần tử lớn nhất
  for (int i = n - 1; i > 0; i--)
  {
    swap(a[0], a[i]);
    Heapify(a, i, 0);
  }
}
Tương tự Merge sort, Heap sort có độ phức tạp của thuật toán O(n*log(n)). Vì vậy nên việc cài đặt và sử dụng Heap sort tương đối khó, và cần phải hiểu về cây nhị phân mới có thể sử dụng tốt.

6. Quicksort

Quicksort (còn được gọi là Sắp xếp nhanh) là 1 dạng thuật toán Divide and Conquer giống như Merge sort. Theo ý kiến cá nhân của mình, thì Quicksort được xem là thuật toán sắp xếp nhanh nhất (vậy nên nó mới có chữ “quick” chứ :v).
Quicksort chọn 1 phần tử trong mảng là chốt (pivot), thường ở vị trí giữa cho dễ tính (ta có thể chọn ở bất kì đâu, nhưng để tránh rơi vào vòng lặp vô tận thì cứ chọn ở giữa đi 😀 ). Ta đưa những phần tử nhỏ hơn pivot về danh sách thứ 1, và những phần tử lớn hơn pivot về danh sách thứ 2. Cứ như vậy, ta dùng đệ quy ở 2 phần danh sách con đến khi nào chỉ còn 1 phần tử.
quick-sort
Mã nguồn:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void QuickSort(int a[], int left, int right)
{
  int i = left, j = right;
  int pivot = a[(left + right) / 2]; // Lấy pivot là phần tử giữa
  do
  {
    // Tìm vị trí i, j cần hoán vị
    while (a[i] < pivot && i < right) i++;
    while (a[j] > pivot && j > left) j--;
    if (i <= j)
    {
      swap(a[i], a[j]);
      i++; j--;
    }
  } while (i <= j);
   
  // Khi đó pivot sẽ chốt vị trí trong mảng
  // Ta cần gọi đệ hàm đến 2 mảng con bên trái pivot và bên phải pivot
  if (left < j) QuickSort(a, left, j);
  if (i < right) QuickSort(a, i, right);
}
Quicksort cũng tương tự như Merge sort, cũng có độ phức tạp thuật toán O(n*log(n)). Tuy nhiên, việc cài đặt Quicksort ngắn gọn hơn hẳn so với Merge sort hay Heap sort, và mình cũng rất hay sử dụng Quicksort.
 nghe câu chữ thì đã thấy thằng Quick Sort có vẻ là nhanh rồi (Quick là nhanh mà :v), và thực tế, thì Quick Sort cũng là đáp án được nhiều người lựa chọn nhất khi được hỏi câu hỏi trên.
Nhưng thực tế, thì lại không phải vậy, phần lớn mọi người đã sai khi lựa chọn Quick Sort là câu trả lời của mình. Vậy đáp án là Tim Sort ư? hay Insertion Sort nhỉ
Cùng nhìn vào bảng thống kê độ phức tạp trung bình của các thuật toán sắp xếp

Nhìn vào bảng trên thì rõ ràng Quick Sort có độ phức tạp trung bình là O(n log(n)), ơ chẳng phải dựa vào kết quả này thì Quick Sort là nhanh nhất còn gì nữa?
Chậm lại 1 chút, chúng ta hãy thử đặt câu hỏi ngược lại ở đây xem sao nhé: "Nếu QuickSort là nhanh nhất thì tại sao lại còn phải đẻ ra ti tỉ các loại thuật toán sắp xếp khác làm cái lề gì nhỉ?"
Tiếp theo, chúng ta sẽ xem tốc độ sắp xếp của các thuật toán dựa theo dữ liệu đầu vào, dữ liệu ở đây có các case từ dữ liệu Random đến Nearly Sorted hay cả việc Reversed Dữ liệu
Nhìn vào thống kê phía trên, có thể thấy với mỗi kiểu dữ liệu khác nhau thì lại có 1 kiểu sắp xếp chiếm ưu thế riêng, ví dụ với dữ liệu Nearly Sorted thì Insertion Sort là nhanh nhất nhưng khi với những kiểu dữ liệu phức tạp hơn thì Insertion Sort lại không phải là nhanh nhất.
Như vậy, từ những thống kê trên chúng ta đã dần dần hình dung ra đáp án cho câu hỏi "Thuật toán sắp xếp nào là nhanh nhất" rồi nhỉ

Vậy câu trả lời đúng là gì?

=> "Không có 1 bất kỳ thuật toán sắp xếp nào cụ thể cả, nó còn phụ thuộc vào nhiều yếu tố"
Và "phụ thuộc vào nhiều yếu tố" cũng là lý do mà có rất nhiều loại thuật toán sắp xếp khác nhau ra đời. Chúng ta nhìn vào 1 vài ví dụ cụ thể dưới đây để thấy những yếu tố nào sẽ ảnh hưởng việc lựa chọn thuật toán
  • Quick Sort sẽ là tốt nhất nếu ...
  1. Không lo lắng về các case đầu vào kể cả trường hợp xấu nhất (trật tự nói chung là ngẫu nhiên)
  2. Không quan tâm đến dung lượng bộ nhớ, bộ nhớ là hoàn toàn lý tưởng và phù hợp ở đây
  • Nếu dữ liệu đã được sắp xếp sẵn, thì nên chọn Insertion Sort hoặc Shell Sort sẽ tốt hơn.
  • Nếu chúng ta thực sự phải loại bỏ case xấu nhất, có thể sử dụng Heap (hoặc ít nhất là Quick3) với độ phức tạp NlogN
  • Tim Sort sẽ có độ phức tạp thấp hơn Quick Sort ở cả Best Case lẫn Worse Case, Tim Sort là sự kết hợp của Merge Sort và Insertion Sort. Python sử dụng thuật toán sắp xếp này là mặc định của họ
  • Trong trường hợp, dữ liệu rất ít phần tử (10-20 phần tử), lựa chọn Selection Sort sẽ nhanh hơn Quick Sort
    Tóm lại 1 lần nữa , về lý thuyết thì Quick Sort thật sự là thuật toán sắp xếp nhanh nhất trong phần lớn các trường hợp. Tuy nhiên, trên thực tế, việc lựa chọn thuật toán sắp xếp dựa vào nhiều yếu tố như dữ liệu đầu vào số lượng như thế nào, có sắp xếp sẵn hay không, dung lượng bộ nhớ ra sao, tốc độ xử lý CPU...

Thuật toán và bài học cuộc sống

Mình vẫn thường hay nói đùa rằng: "Code không bao giờ lừa dối chúng ta cả". Và thực sự thì khi Code mình cũng chiêm nghiệm ra nhiều bài học cuộc sống cho chính mình luôn. Ở đây, từ một câu hỏi thuật toán sắp xếp vô cùng đơn giản nhưng chúng ta có thể rút ra được rất nhiều bài học thực tế:
  • Hãy học cách đặt lại câu hỏi cho vấn để đang được hỏi, để từ đó phân tích tìm ra câu trả lời chính xác nhất. Đôi khi làm dự án thực tế, khách hàng sẽ đưa ra những yêu cầu mơ hồ, thay vì cắp đầu vào tìm giải pháp, hay code thì chúng ta hãy hỏi rõ khách hàng, làm rõ vấn đề đó trước đã
  • Trong cuộc sống, không có gì là hoàn hảo cả hãy nhìn đặt vấn đề gặp phải dưới nhiều góc nhìn khác nhau, để cân nhắc và lựa chọn giải pháp cho hợp lý

Nhận xét

Bài đăng phổ biến từ blog này

Entry Test của FPT

IQ - Kiểm tra tư duy logic (8/20) - GMAT- Kiểm tra khả năng tính toán trong thời gian ngắn (8/20) - Tiếng Anh (18-> 25/50) - Các bài thi chuyên môn - FE (8/20) IQ: lên mạng tìm "IQ test" là ra đầy. + GMAT: Những câu trắc nghiệm tính toán đơn giản kiểu như sau:  1 . Một shop thời trang sale off quần jeans 15 %, quần jeans giá 450 $, người mua đưa 500 $, hỏi cashier trả lại bao nhiêu $ tiền thừa.? 2 . 100 % là 180 , vậy 150 là bao nhiêu %? Tiếng anh: Cỡ như thi TOEIC thôi. Chuyên môn: Mobile thì trắc nghiệm Java. Qúa trình tuyển như sau :v Lần 1: Test IQ, Tiếng Anh( mình làm í ẹ khoảng 50% mà vẫn được) , Java Lân 2: được gọi điện lên :)) + Gioi thiệu bản thân + Họ chỉ hỏi các câu căn bản như: -. OOP: là gì, 4 tính chất, ví dụ, khác nhau giữa interface và abstract - CODE: hầu toàn các bài toán vòng for :)) , cẩn thận mấy câu kế thừa. SQL (distinct, view, function, cursor, store procedure, ...v.v.), nhớ có câu cộng 2 số int không dùng biến đệm hơi khoai haha + Nói ch...

Java: Java Package-Thư viện trong Java

Giới thiệu về Package Các bạn mới học lập trình Java thường không dể ý tới package vì các bạn toàn tạo file .java vào cùng 1 chỗ, không cần sắp xếp, không cần quản lý truy nhập. Nhưng để tăng kỹ năng lập trình với Java, các bạn cần phải tìm hiểu về package trong Java. Các bạn có thể tham khảo định nghĩa sau: Package được dùng để đóng gói các lớp trong chương trình lại với nhau thành một khối. Đây là cách tốt nhất để lưu trữ các lớp gần giống nhau hoặc có cùng một module thành một khối thống nhất – để đáp ứng 1 khối chức năng. Từ đây mình sẽ giới thiệu thêm với các bạn các câu lệnh nhâp khẩu,nó có định dạng như sau : Định dạng :  import javaPackageNameImport;    Nó giống như khai báo thư viện ở các ngôn ngữ lập trình khác.Như vậy,chỉ khi các bạn nhập khẩu chúng,các bạn mới có thể sử dụng thư viện mà chúng cung cấp cho ta. VD :    import java.util.Date;   import java.text.SimpleDateFormat; Lưu ý : -Các câu lệnh nhập khẩu rất nhiều và...

phỏng vấn Embedded C và C++?

Có vài điểm rất nhỏ, các bạn không để ý có thể dẫn đến bất lợi (nếu không muốn nói là rớt) ở vòng hồ sơ hoặc khi đi phỏng vấn. Mình liệt kê rất cụ thể ra những chuyện mình đã thấy qua nhiều lần (người thật, việc thật), hy vọng cho các bạn thêm kinh nghiệm. Lưu ý: Những chuyện này rất chủ quan, có thể chỉ đúng trong môi trường của mình và hoàn toàn không đúng trong môi trường khác. Gửi email CV đến không có Cover Letter, chỉ đính kèm mỗi file PDF là cái CV. CV ghi : tiếng Anh: Trung Bình, C: Trung Bình,... nhưng bên dưới lại ghi ưu điểm_:  là người ham học hỏi... CV bằng tiếng Việt và lại viết sai chính tả tiếng Việt. CV ghi quá nhiều về các hoạt động tình nguyện, hiến máu, blah blah... nhưng phần kĩ thuật lại có 3 dòng thôi. Mặc đồ thể dục của trường và đi dép lê đến PV. Đến muộn PV (em ấy bảo bị kẹt xe). Đưa yêu cầu, em ấy không hiểu đề nhưng ngại không dám hỏi lại, dẫn đến viết hoàn toàn sai. Hỏi ngay câu căn bản đầu tiên em ấy đã nói không biết và  đổ do trường kh...