Trong bài viết này, chúng ta sẽ khám phá về thuật toán sắp xếp trộn (Merge Sort). Đây là một thuật toán phổ biến và hiệu quả trong lĩnh vực khoa học máy tính, được sử dụng để sắp xếp dữ liệu một cách hiệu quả và nhanh chóng. Chúng ta sẽ tìm hiểu về cách thức hoạt động của thuật toán, phân tích độ phức tạp của nó, cùng những ưu nhược điểm và ứng dụng thực tế.
Khái niệm thuật toán sắp xếp trộn
Sắp xếp trộn (tiếng Anh: Merge Sort) là một thuật toán sắp xếp đệ quy dựa trên nguyên lý "chia để trị" (divide and conquer). Thuật toán này hoạt động bằng cách bẻ danh sách cần sắp xếp thành đôi, sau đó ghép các mảnh này lại với nhau để tạo ra một danh sách đã sắp xếp. Quá trình trộn được thực hiện bằng cách so sánh các phần tử từng cặp và di chuyển chúng vào vị trí đúng.
Cách thức hoạt động của thuật toán sắp xếp trộn
Thuật toán sắp xếp trộn được thực hiện qua các bước sau:
- Bước 1: Xác định điểm đứng giữa danh sách.
- Bước 2: Từ điểm đứng giữa đó tách danh sách thành hai phần bằng nhau (hoặc gần bằng nhau)
- Bước 3: Dùng đệ quy để tiếp tục bẻ đôi các nửa này cho đến khi chỉ còn duy nhất một phần tử.
- Bước 4: Ghép các phần tử đã bẻ đôi lại với nhau theo thứ tự tăng dần hoặc giảm dần.
Giả sử ta có mảng arr = {45, 12, 48, 19, 36, 14, 30, 69}. Để sắp xếp mảng này theo thứ tự tăng dần, ta thực hiện thuật toán như sau:
- Đầu tiên ta nhận mảng đầu vào, sau đó bẻ mảng ra làm đôi tại vị trí chia cắt mảng.
- Sau đó, ta tiếp tục bẻ đôi hai nửa thành các mảnh nhỏ hơn.
- Ta tiếp tục tách các mảnh nhỏ ra thành các mảnh nhỏ hơn.
- Bây giờ ta có các mảng chỉ có duy nhất một phần tử. Tiếp theo ta sẽ so sánh các mảnh và ghép chúng vào mảng đã được sắp xếp. Cách làm khá đơn giản, các bạn hãy tưởng tượng có 10 mẫu giấy đánh số từ 1 đến 10 và việc của bạn là ghép chúng lại sao cho các bạn được một dãy số đã sắp xếp tăng dần từ 1 đến 10, các bạn làm sao? Đơn giản là phần tử nào nhỏ hơn sẽ được ghép vào. Thuật toán này cũng tương tự như vậy.
- Tương tự như vậy, ta so sánh từng phần tử của các mảnh và ghép chúng vào mảng đã được sắp xếp.
- Cuối cùng, ta ghép hai mảnh vào và ta được mảng đã được sắp xếp ở dưới.
Triển khai thuật toán
Sau đây là cách triển khai thuật toán sắp xếp trộn mà bạn có thể tham khảo.
C
#include <stdio.h>// Hàm xuất mảngvoid printArr(int arr[], int arrLength){for (int i = 0; i < arrLength; i++){printf("%d ", arr[i]);}printf("\n");}void merge(int arr[], int leftArr[], int rightArr[], int leftLength, int rightLength);// Hàm sắp xếpvoid mergeSort(int arr[], int arrLength){// Cho một cái base case vì đây là hàm đệ quyif (arrLength <= 1){return;}// Tạo hai mảng phụ để lưu trữ hai mảnh đã bẻ đôiint mid = arrLength / 2; // Xác định vị trí để chia cắtint leftLength = mid, rightLength = arrLength - mid; // Độ dài hai nửaint leftArr[leftLength];int rightArr[rightLength];// Đưa các phần tử từ mảng gốc vào hai mảng phụint rightIndex = 0;for (int i = 0; i < arrLength; i++){// Phần bên trái vị trí chia cắt (i < mid) được đưa vào mảng leftArrif (i < mid){leftArr[i] = arr[i];}// Phần bên phải vị trí chia cắt (i >= mid) được đưa vào mảng rightArrelse{rightArr[rightIndex] = arr[i];rightIndex++}}// Gọi đệ quy để tiếp tục sắp xếpmergeSort(leftArr, leftLength);mergeSort(rightArr, rightLength);// Gọi hàm merge để ghép hai mảnh lại với nhau thành mảng đã được sắp xếpmerge(arr, leftArr, rightArr, leftLength, rightLength);}// Hàm để ghép các mảnh lại với nhauvoid merge(int arr[], int leftArr[], int rightArr[], int leftLength, int rightLength) {int leftIndex = 0; // Biến giữ vị trí phần tử được so sánh ở mảnh bên tráiint rightIndex = 0; // Biến giữ vị trí phần tử được so sánh ở mảnh bên phảiint i = 0; // Biến giữ vị trí tiếp theo được ghép vào ở mảng arr// Vòng lặp while để so sánh và ghép các phần tử lại với nhauwhile (leftIndex < leftLength && rightIndex < rightLength){// Phần tử nào nhỏ hơn thì ghép vàoif (leftArr[leftIndex] < rightArr[rightIndex]){arr[i] = leftArr[leftIndex];leftIndex++;}else{arr[i] = rightArr[rightIndex];rightIndex++;}i++;}// Ghép các phần tử dư thừa vào mảng arr nếu cówhile (leftIndex < leftLength){arr[i] = leftArr[leftIndex];leftIndex++;i++;}while (rightIndex < rightLength){arr[i] = rightArr[rightIndex];rightIndex++;i++;}}int main(int argc, char const *argv[]){int arr[] = {45, 12, 48, 19, 36, 14, 30, 69};int arrLength = sizeof(arr) / sizeof(arr[0]);printf("Mang ban dau: ");printArr(arr, arrLength);mergeSort(arr, arrLength);printf("Mang da duoc sap xep: ");printArr(arr, arrLength);return 0;}
C++
#include <iostream>using namespace std;// Hàm xuất mảngvoid printArr(int arr[], int arrLength){for (int i = 0; i < arrLength; i++){cout << arr[i] << " ";}cout << endl;}void merge(int arr[], int leftArr[], int rightArr[], int leftLength, int rightLength);// Hàm sắp xếpvoid mergeSort(int arr[], int arrLength){// Cho một cái base case vì đây là hàm đệ quyif (arrLength <= 1){return;}// Tạo hai mảng phụ để lưu trữ hai mảnh đã bẻ đôiint mid = arrLength / 2; // Xác định vị trí để chia cắtint leftLength = mid, rightLength = arrLength - mid; // Độ dài hai nửaint leftArr[leftLength];int rightArr[rightLength];// Đưa các phần tử từ mảng gốc vào hai mảng phụint rightIndex = 0;for (int i = 0; i < arrLength; i++){// Phần bên trái vị trí chia cắt (i < mid) được đưa vào mảng leftArrif (i < mid){leftArr[i] = arr[i];}// Phần bên phải vị trí chia cắt (i >= mid) được đưa vào mảng rightArrelse{rightArr[rightIndex] = arr[i];rightIndex++}}// Gọi đệ quy để tiếp tục sắp xếpmergeSort(leftArr, leftLength);mergeSort(rightArr, rightLength);// Gọi hàm merge để ghép hai mảnh lại với nhau thành mảng đã được sắp xếpmerge(arr, leftArr, rightArr, leftLength, rightLength);}// Hàm để ghép các mảnh lại với nhauvoid merge(int arr[], int leftArr[], int rightArr[], int leftLength, int rightLength) {int leftIndex = 0; // Biến giữ vị trí phần tử được so sánh ở mảnh bên tráiint rightIndex = 0; // Biến giữ vị trí phần tử được so sánh ở mảnh bên phảiint i = 0; // Biến giữ vị trí tiếp theo được ghép vào ở mảng arr// Vòng lặp while để so sánh và ghép các phần tử lại với nhauwhile (leftIndex < leftLength && rightIndex < rightLength){// Phần tử nào nhỏ hơn thì ghép vàoif (leftArr[leftIndex] < rightArr[rightIndex]){arr[i] = leftArr[leftIndex];leftIndex++;}else{arr[i] = rightArr[rightIndex];rightIndex++;}i++;}// Ghép các phần tử dư thừa vào mảng arr nếu cówhile (leftIndex < leftLength){arr[i] = leftArr[leftIndex];leftIndex++;i++;}while (rightIndex < rightLength){arr[i] = rightArr[rightIndex];rightIndex++;i++;}}int main(int argc, char const *argv[]){int arr[] = {45, 12, 48, 19, 36, 14, 30, 69};int arrLength = sizeof(arr) / sizeof(arr[0]);cout << "Mang ban dau: ";printArr(arr, arrLength);mergeSort(arr, arrLength);cout << "Mang da duoc sap xep: ";printArr(arr, arrLength);return 0;}
C#
using System;namespace Main{class Program{// Phương thức xuất mảngstatic void printArr(int[] arr){foreach (int i in arr){Console.Write($"{i} ");}Console.WriteLine();}// Phương thức sắp xếpstatic void mergeSort(int[] arr){// Cho một cái base case vì đây là hàm đệ quyif (arr.Length <= 1){return;}// Tạo hai mảng phụ để lưu trữ hai mảnh đã bẻ đôiint mid = arr.Length / 2; // Xác định vị trí để chia cắtint[] leftArr = new int[mid];int[] rightArr = new int[arr.Length - mid];// Đưa các phần tử từ mảng gốc vào hai mảng phụint rightIndex = 0;for (int i = 0; i < arr.Length; i++){// Phần bên trái vị trí chia cắt (i < mid) được đưa vào mảng leftArr// Phần bên phải vị trí chia cắt (i >= mid) được đưa vào mảng rightArrif (i < mid){leftArr[i] = arr[i];}else{rightArr[rightIndex] = arr[i];rightIndex++;}}// Gọi đệ quy để tiếp tục sắp xếpmergeSort(leftArr);mergeSort(rightArr);// Gọi hàm merge để ghép hai mảnh lại với nhau thành mảng đã được sắp xếpmerge(arr, leftArr, rightArr);}// Phương thức để ghép các mảnh lại với nhaustatic void merge(int[] arr, int[] leftArr, int[] rightArr){int leftIndex = 0; // Biến giữ vị trí phần tử được so sánh ở mảnh bên tráiint rightIndex = 0;// Biến giữ vị trí phần tử được so sánh ở mảnh bên phảiint i = 0; // Biến giữ vị trí tiếp theo được ghép vào ở mảng arr// Vòng lặp while để so sánh và ghép các phần tử lại với nhauwhile (leftIndex < leftArr.Length && rightIndex < rightArr.Length){// Phần tử nào nhỏ hơn thì ghép vàoif (leftArr[leftIndex] < rightArr[rightIndex]){arr[i] = leftArr[leftIndex];leftIndex++;}else{arr[i] = rightArr[rightIndex];rightIndex++;}i++;}// Ghép các phần tử dư thừa vào mảng arr nếu cówhile (leftIndex < leftArr.Length){arr[i] = leftArr[leftIndex];leftIndex++;i++;}while (rightIndex < rightArr.Length){arr[i] = rightArr[rightIndex];rightIndex++;i++;}}static void Main(){int[] arr = {45, 12, 48, 19, 36, 14, 30, 69};Console.Write("Mang ban dau: ");printArr(arr);mergeSort(arr);Console.Write("Mang da duoc sap xep: ");printArr(arr);}}}
Java
import java.util.Arrays;public class Main {// Phương thức sắp xếppublic static void mergeSort(int[] arr) {// Cho một cái base case vì đây là hàm đệ quyif (arr.length <= 1) {return;}// Tạo hai mảng phụ để lưu trữ hai mảnh đã bẻ đôiint mid = arr.length / 2;int[] leftArr = new int[mid];int[] rightArr = new int[arr.length - mid];// Đưa các phần tử từ mảng gốc vào hai mảng phụint rightIndex = 0;for (int i = 0; i < arr.length; i++) {// Phần bên trái vị trí chia cắt (i < mid) được đưa vào mảng leftArr// Phần bên phải vị trí chia cắt (i >= mid) được đưa vào mảng rightArrif (i < mid) {leftArr[i] = arr[i];} else {rightArr[rightIndex] = arr[i];rightIndex++;}}// Gọi đệ quy để tiếp tục sắp xếpmergeSort(leftArr);mergeSort(rightArr);// Gọi hàm merge để ghép hai mảnh lại với nhau thành mảng đã được sắp xếpmerge(arr, leftArr, rightArr);}// Phương thức để ghép các mảnh lại với nhauprivate static void merge(int[] arr, int[] leftArr, int[] rightArr) {int leftIndex = 0; // Biến giữ vị trí phần tử được so sánh ở mảnh bên tráiint rightIndex = 0; // Biến giữ vị trí phần tử được so sánh ở mảnh bên phảiint i = 0; // Biến giữ vị trí tiếp theo được ghép vào ở mảng arr// Vòng lặp while để so sánh và ghép các phần tử lại với nhauwhile (leftIndex < leftArr.length && rightIndex < rightArr.length) {// Phần tử nào nhỏ hơn thì ghép vàoif (leftArr[leftIndex] < rightArr[rightIndex]) {arr[i] = leftArr[leftIndex];leftIndex++;} else {arr[i] = rightArr[rightIndex];rightIndex++;}i++;}// Ghép các phần tử dư thừa vào mảng arr nếu cówhile (leftIndex < leftArr.length) {arr[i] = leftArr[leftIndex];leftIndex++;i++;}while (rightIndex < rightArr.length) {arr[i] = rightArr[rightIndex];rightIndex++;i++;}}public static void main(String[] args) {int[] arr = {45, 12, 48, 19, 36, 14, 30, 69};System.out.println("Mang ban dau: " + Arrays.toString(arr));mergeSort(arr);System.out.println("Mang da duoc sap xep: " + Arrays.toString(arr));}}
Python
# Hàm sắp xếpdef mergeSort(lst):# Cho một cái base case vì đây là hàm đệ quyif len(lst) <= 1:return# Tạo hai danh sách phụ để lưu trữ hai mảnh đã bẻ đôimid = len(lst) // 2leftLst = lst[:mid]rightLst = lst[mid:]# Gọi đệ quy để tiếp tục sắp xếpmergeSort(leftLst)mergeSort(rightLst)# Gọi hàm merge để ghép hai mảnh lại với nhau thành danh sách đã được sắp xếpmerge(lst, leftLst, rightLst)# Hàm để ghép các mảnh lại với nhaudef merge(lst, leftLst, rightLst):leftIndex = 0 # Biến giữ vị trí phần tử được so sánh ở mảnh bên tráirightIndex = 0 # Biến giữ vị trí phần tử được so sánh ở mảnh bên phảii = 0 # Biến giữ vị trí tiếp theo được ghép vào ở mảng arr# Vòng lặp while để so sánh và ghép các phần tử lại với nhauwhile leftIndex < len(leftLst) and rightIndex < len(rightLst):# Phần tử nào nhỏ hơn thì ghép vàoif leftLst[leftIndex] < rightLst[rightIndex]:lst[i] = leftLst[leftIndex]leftIndex += 1else:lst[i] = rightLst[rightIndex]rightIndex += 1i += 1# Ghép các phần tử dư thừa vào mảng arr nếu cówhile leftIndex < len(leftLst):lst[i] = leftLst[leftIndex]leftIndex += 1i += 1while rightIndex < len(rightLst):lst[i] = rightLst[rightIndex]rightIndex += 1i += 1lst = [45, 12, 48, 19, 36, 14, 30, 69]print(f"Danh sách ban đầu: {lst}")mergeSort(lst)print(f"Danh sách đã được sắp xếp: {lst}")
JavaScript
// Hàm sắp xếpfunction mergeSort(arr) {// Cho một cái base case vì đây là hàm đệ quyif (arr.length <= 1) {return;}// Tạo hai mảng phụ để lưu trữ hai mảnh đã bẻ đôilet mid = Math.floor(arr.length / 2);let leftArr = arr.slice(0, mid);let rightArr = arr.slice(mid);// Gọi đệ quy để tiếp tục sắp xếpmergeSort(leftArr);mergeSort(rightArr);// Gọi hàm merge để ghép hai mảnh lại với nhau thành mảng đã được sắp xếpmerge(arr, leftArr, rightArr);}// Hàm để ghép các mảnh lại với nhaufunction merge(arr, leftArr, rightArr) {let leftIndex = 0; // Biến giữ vị trí phần tử được so sánh ở mảnh bên tráilet rightIndex = 0; // Biến giữ vị trí phần tử được so sánh ở mảnh bên phảilet i = 0; // Biến giữ vị trí tiếp theo được ghép vào ở mảng arr// Vòng lặp while để so sánh và ghép các phần tử lại với nhauwhile (leftIndex < leftArr.length && rightIndex < rightArr.length) {// Phần tử nào nhỏ hơn thì ghép vàoif (leftArr[leftIndex] < rightArr[rightIndex]) {arr[i] = leftArr[leftIndex];leftIndex++;} else {arr[i] = rightArr[rightIndex];rightIndex++;}i++;}// Ghép các phần tử dư thừa vào mảng arr nếu cówhile (leftIndex < leftArr.length) {arr[i] = leftArr[leftIndex];leftIndex++;i++;}while (rightIndex < rightArr.length) {arr[i] = rightArr[rightIndex];rightIndex++;i++;}}let arr = [45, 12, 48, 19, 36, 14, 30, 69];window.alert("Mảng ban đầu: " + arr);mergeSort(arr);window.alert("Mảng đã được sắp xếp: " + arr);
Phân tích độ phức tạp về thời gian và bộ nhớ
Độ phức tạp về thời gian
Trung bình và trong trường hợp xấu nhất, thuật toán sắp xếp trộn có độ phức tạp về thời gian là O(n log n). Điều này có nghĩa là thời gian thực hiện của thuật toán tăng theo tốc độ hợp lý khi số lượng phần tử cần sắp xếp tăng lên. Điều này giúp thuật toán sắp xếp trộn hiệu quả khi xử lý dữ liệu lớn.
Độ phức tạp thời gian của sắp xếp trộn ổn định và không phụ thuộc vào trạng thái ban đầu của dữ liệu được sắp xếp. Dù dữ liệu đã sắp xếp hoặc chưa sắp xếp, thời gian thực hiện của thuật toán vẫn không thay đổi đáng kể.
Độ phức tạp về bộ nhớ
Thuật toán sắp xếp trộn sử dụng mảng phụ để lưu trữ các phần tử trong quá trình ghép các phần tử. Tổng kích thước của mảng phụ là bằng với kích thước của mảng ban đầu, do đó độ phức tạp về bộ nhớ là O(n).
Trong trường hợp cần sắp xếp một danh sách liên kết, không có yêu cầu về bộ nhớ liên tục, và thuật toán có thể sử dụng một danh sách liên kết phụ để ghép các nút lại.
Tuy nhiên, việc sử dụng mảng phụ có thể gây ra sự lãng phí bộ nhớ đối với các dữ liệu lớn. Điều này cần được lưu ý trong các ứng dụng có hạn chế về bộ nhớ.
Ưu điểm và hạn chế của thuật toán sắp xếp trộn
Ưu điểm
- Nhanh, có hiệu suất cao với độ phức tạp thời gian ổn định: Như đã phân tích trên, thuật toán sắp xếp trộn có độ phức tạp về thời gian là O(n log n), giúp cho thuật toán trở nên rất nhanh với dữ liệu khổng lồ và thời gian không bị phí.
- Được sử dụng rộng rãi trong các ứng dụng yêu cầu sắp xếp dữ liệu lớn: Vì ưu điểm nói trên mà thuật toán sắp xếp trộn được sử dụng rất rộng rãi để tối ưu thời gian sắp xếp.
- Ổn định: Merge Sort là một thuật toán ổn định, có nghĩa là các phần tử bằng nhau không thay đổi thứ tự sau khi được sắp xếp. Điều này làm cho Merge Sort phù hợp trong các tình huống yêu cầu bảo toàn sự ổn định của dữ liệu.
Hạn chế
- Tốn bộ nhớ: Như đã phân tích trên, thuật toán sắp xếp trộn có độ phức tạp về bộ nhớ O(n), vì thuật toán yêu cầu thêm bộ nhớ trong quá trình sắp xếp. Điều này có thể làm tăng sự sử dụng bộ nhớ và gây lãng phí, đặc biệt là khi xử lý các tập dữ liệu lớn. Việc quản lý bộ nhớ là một yếu tố cần được lưu ý khi triển khai sắp xếp trộn.
- Không phải lúc nào cũng tối ưu hóa được với danh sách gần như đã sắp xếp hoặc đã sắp xếp hoàn toàn: Trong trường hợp dữ liệu đã sắp xếp hoặc gần như đã sắp xếp, thuật toán sắp xếp trộn vẫn phải thực hiện quá trình chia nhỏ và trộn, dẫn đến một số thao tác không cần thiết.
Ứng dụng
Thuật toán sắp xếp trộn được sử dụng rộng rãi trong nhiều lĩnh vực, bao gồm:
- Cơ sở dữ liệu: Sắp xếp trộn được sử dụng để sắp xếp các bảng và chỉ mục trong cơ sở dữ liệu.
- Hợp nhất danh sách: Đối với các ứng dụng yêu cầu hợp nhất danh sách từ nhiều nguồn, thuật toán sắp xếp trộn là một lựa chọn lý tưởng.
- Đa luồng và song song: Sắp xếp trộn có thể được song song hóa và áp dụng cho các bài toán đa luồng,đặc biệt là trong việc sắp xếp và ghép kết quả từ nhiều luồng dữ liệu đồng thời.
Kết
Trên đây là một cái nhìn tổng quan về thuật toán sắp xếp trộn (Merge Sort). Với cách thức hoạt động đơn giản và hiệu suất cao, thuật toán này đã trở thành một công cụ quan trọng trong lĩnh vực khoa học máy tính. Việc hiểu và áp dụng thuật toán này sẽ giúp bạn nắm bắt được cách sắp xếp dữ liệu một cách hiệu quả và tối ưu. Ngoài ra, ứng dụng của thuật toán trong nhiều lĩnh vực khác nhau cũng là một điểm mạnh của nó. Hy vọng rằng bài viết này đã giúp bạn hiểu rõ hơn về thuật toán sắp xếp trộn và giá trị của nó trong công việc và nghiên cứu của bạn.
Tags:
dsa