十大排序算法可视化:
Data Structure Visualization (usfca.edu)
十大排序算法定义及代码
-
冒泡排序(Bubble Sort):依次比较相邻的元素,如果顺序不对则交换,每一轮将最大(或最小)元素冒泡到末尾。
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1]);
}
}
}
}
-
选择排序(Selection Sort):从未排序的元素中选择最小(或最大)的元素,并将其放入已排序部分的末尾。
void selectionSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
swap(arr[i], arr[minIndex]);
}
}
-
插入排序(Insertion Sort):将未排序的元素逐个插入到已排序部分的正确位置,直到所有元素都被排序。
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
-
希尔排序(Shell Sort):插入排序的改进版本,通过将数组分成多个较小的子数组来进行排序,最终进行一次完整的插入排序。
void shellSort(int arr[], int n) {
for (int gap = n / 2; gap > 0; gap /= 2) {
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i;
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
-
归并排序(Merge Sort):采用分治法,将数组分成两个子数组,分别对子数组进行排序,然后将排序后的子数组合并成一个有序数组。
void merge(int arr[], int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int* leftArr = new int[n1];
int* rightArr = new int[n2];
for (int i = 0; i < n1; i++) {
leftArr[i] = arr[left + i];
}
for (int j = 0; j < n2; j++) {
rightArr[j] = arr[mid + 1 + j];
}
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (leftArr[i] <= rightArr[j]) {
arr[k] = leftArr[i];
i++;
} else {
arr[k] = rightArr[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = leftArr[i];
i++;
k++;
}
while (j < n2) {
arr[k] = rightArr[j];
j++;
k++;
}
delete[] leftArr;
delete[] rightArr;
}
void mergeSort(int arr[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
-
快速排序(Quick Sort):选取一个基准元素,将数组分成左右两个子数组,使左子数组中的元素都小于基准元素,右子数组中的元素都大于基准元素,然后递归地对左右子数组进行快速排序。
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j <= high - 1; j++) {
if (arr[j] <= pivot) {
i++;
swap(arr[i], arr[j]);
}
}
swap(arr[i + 1], arr[high]);
return i + 1;
}
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
-
堆排序(Heap Sort):构建一个最大(或最小)堆,然后将堆顶元素与堆的最后一个元素交换,并对剩余的元素进行堆调整,重复该过程直到所有元素都被排序。
void heapify(int arr[], int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr[i], arr[largest]);
heapify(arr, n, largest);
}
}
void heapSort(int arr[], int n) {
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
for (int i = n - 1; i > 0; i--) {
swap(arr[0], arr[i]);
heapify(arr, i, 0);
}
}
-
计数排序(Counting Sort):根据待排序数组中的元素大小,统计每个元素出现的次数,然后根据统计信息进行排序。
void countingSort(int arr[], int n, int k) {
int* count = new int[k + 1]();
int* output = new int[n];
for (int i = 0; i < n; i++) {
count[arr[i]]++;
}
for (int i = 1; i <= k; i++) {
count[i] += count[i - 1];
}
for (int i = n - 1; i >= 0; i--) {
output[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
for (int i = 0; i < n; i++) {
arr[i] = output[i];
}
delete[] count;
delete[] output;
}
-
桶排序(Bucket Sort):将待排序元素分到不同的桶中,每个桶单独排序,然后按顺序将各个桶中的元素合并。
--------》
void insertionSort(vector<int>& bucket) {
int n = bucket.size();
for (int i = 1; i < n; i++) {
int key = bucket[i];
int j = i - 1;
while (j >= 0 && bucket[j] > key) {
bucket[j + 1] = bucket[j];
j--;
}
bucket[j + 1] = key;
}
}
void bucketSort(vector<int>& arr) {
int n = arr.size();
vector<vector<int>> buckets(n);
for (int i = 0; i < n; i++) {
int bucketIndex = n * arr[i];
buckets[bucketIndex].push_back(arr[i]);
}
for (int i = 0; i < n; i++) {
insertionSort(buckets[i]);
}
int index = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < buckets[i].size(); j++) {
arr[index++] = buckets[i][j];
}
}
}
-
基数排序(Radix Sort):将待排序元素按照位数从低到高进行排序,每个位数上使用稳定的排序算法,如计数排序或桶排序。
int getMax(int arr[], int n) {
int maxVal = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > maxVal) {
maxVal = arr[i];
}
}
return maxVal;
}
void countingSortByDigit(int arr[], int n, int exp) {
int output[n];
int count[10] = {0};
for (int i = 0; i < n; i++) {
count[(arr[i] / exp) % 10]++;
}
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
for (int i = n - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
for (int i = 0; i < n; i++) {
arr[i] = output[i];
}
}
void radixSort(int arr[], int n) {
int maxVal = getMax(arr, n);
for (int exp = 1; maxVal / exp > 0; exp *= 10) {
countingSortByDigit(arr, n, exp);
}
}
十大排序时间复杂度:
以下是十大常见排序算法的时间复杂度的表格形式输出:
排序算法 | 平均时间复杂度 | 最好情况时间复杂度 | 最坏情况时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序(Bubble Sort) | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
选择排序Selection Sort) | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序(Insertion Sort) | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
希尔排序 (Shell Sort) | O(n log n) | O(n log n) | O(n^2) | O(1) | 不稳定 |
归并排序 (Merge Sort) | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
快速排序 (Quick Sort) | O(n log n) | O(n log n) | O(n^2) | O(log n) | 不稳定 |
堆排序 (Heap Sort) | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
计数排序Counting Sort) | O(n + k) | O(n + k) | O(n + k) | O(n + k) | 稳定 |
桶排序 (Bucket Sort) | O(n + k) | O(n + k) | O(n^2) | O(n + k) | 稳定 |
基数排序 (Radix Sort) | O(nk) | O(nk) | O(nk) | O(n + k) | 稳定 |
总体的评价以及应用:
(1)排序算法的性能取决于什么?
取决于算法中比较和交换的次数以及是否需要额外的空间用于存放临时值(如果数据很大会遇上空间不够的问题就会出错)。
(2)什么样的排序算法是稳定的呢?
稳定其实就是在比较过程中,关键字(数组元素的值)相同的两个元素不会发生交换。而有时候我们比较的关键字不是那些对象的唯一属性,也许比较的关键字相同但是对象的其他属性不同,如果这时交换了关键字(数组元素的值)相同的两个对象,那么对象的其他属性也会跟着变化,那就不好了。
保证排序前两个相等的数据其在序列中的先后位置顺序与排序后它们两个先后位置顺序相同
举个选择排序的例子:序列5 8 5 2 9
, 我们知道第一趟选择第1个元素5会与2进行交换,那么原序列中两个5的相对先后顺序也就被破坏了。
8种常用排序算法稳定性分析_排序稳定性_流烟默的博客-优快云博客
(3)排序使用条件
(1)当数据规模较小时候,可以使用简单的直接插入排序或者直接选择排序。
(2)当文件的初态已经基本有序,可以用直接插入排序和冒泡排序。
(3)当数据规模较大时,应用速度最快的排序算法,可以考虑使用快速排序。当记录随机分布的时候,快速排序平均时间最短,但是会出现最坏的情况,这个时候的时间复杂度是O(n^2),且递归深度为n,所需的占空间为O(n)。
(4)堆排序不会出现快排那样最坏情况,且堆排序所需的辅助空间比快排要少,但是这两种算法都不是稳定的,要求排序时是稳定的,可以考虑用归并排序。
(5)归并排序可以用于内部排序,也可以使用于外部排序。在外部排序时,通常采用多路归并,并且通过解决长顺串的合并,加上长的初始串,提高主机与外设并行能力等,以减少访问外存额外次数,提高外排的效率。
(6)特殊的桶排序、基数排序都是稳定且高效的排序算法,但有一定的局限性:
1、关键字可分解。
2、记录的关键字位数较少,如果密集更好
3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。
面试问题总结:
针对数据规模较大,应用速度需求快,稳定性不要求,一般使用快速排序,且快速排序在随机分布记录的情况,可能出现最坏情况,时间On*n;
堆排序不会出现最坏情况,对于额外空间问题O1,但是针对稳定性要求高的场合,QS和堆排都不稳定。
归并排序适用于数据规模大,应用速度高,且稳定性高的场合,对于外部排序情况,可以通过多路归并,提高主机与外设的并行能力,减少外存访问次数,提高外排效率。