插入排序
基本思想是每次将一个待排序的记录按其关键字大小插入前面已经排好序的子序列,直到全部记录插入完成。
1.直接插入排序
代码实现
void insertSort(int *arr, int arrSize) { for (int i = 1; i < arrSize; i++) { if (arr[i] < arr[i - 1]) { int temp = arr[i]; int j = i - 1; while (arr[j] > temp) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = temp; } } }
|
复杂度分析
- 时间复杂度:\(O(n^2)\);
- 向有序子表中逐个插入元素的操作进行了 \(n -
1\) 趟,每趟操作都分为比较关键字和移动元素。
- 最好的情况是元素有序,此时每插入一个元素,都只需比较一次而不用移动元素,时间复杂度为
$ O(n) $;
- 最坏的情况是元素逆序,总的比较次数为 $(1 + 2 + 3 +
··· + n - 1) $ ,总的移动次数为 \((1 + 2 +
··· + n - 1 + 2)\);
- 空间复杂度:$ O(1)$;
- 算法稳定性:稳定
- 由于每次插入元素时总是从后向前先比较再移动,所以不会出现相同元素相对位置发生变化的情况。
2.折半插入排序
代码实现
void insertSort(int *arr, int arrSize) { for (int i = 1; i < arrSize; i++) { int temp = arr[i]; int low = 0, high = i - 1; while (low <= high) { int mid = low + (high - low) / 2; if (temp < arr[mid]) high = mid - 1; else low = mid + 1; } for (int j = i - 1; j >= low; --j) { arr[j + 1] = arr[j]; } arr[low] = temp; } }
|
复杂度分析
- 时间复杂度:\(O(n^2)\);
- 折半插入排序仅减少了比较元素的次数,比较次数约为 \((nlog_2 n)\)
- 比较次数与待排序表的初始状态无关,仅取决于表中的元素个数 \(n\);
- 空间复杂度:$ O(1)$;
- 算法稳定性:稳定
3.希尔排序
先将待排序表分割成若干形如 $ L[i, i + d, i + 2d, ··· , i + kd]$ 的
“特殊”子表,即把相隔某个“增量”的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序。
代码实现
void shellSort(int *arr, int arrSize) { for (int dk = arrSize / 2; dk >= 1; dk /= 2) { for (int i = dk; i < arrSize; i++) { if (arr[i] < arr[i - dk]) { int temp = arr[i]; int j = i - dk; while (j >= 0 && arr[j] > temp) { arr[j + dk] = arr[j]; j -= dk; } arr[j + dk] = temp; } } } }
|
复杂度分析
- 时间复杂度:
- 希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难
- 当 \(n\)
在某个特定范围内,希尔排序的时间复杂度约为 \(O(n^{1.3})\);
- 最坏情况下希尔排序的时间复杂度为 \(O(n^2)\);
- 空间复杂度:\(O(1)\);
- 算法稳定性:不稳定
- 当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序。
交换排序
所谓交换,是指根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。
4.冒泡排序
代码实现
void bubbleSort(int *arr, int arrSize) { for (int i = 0; i < arrSize - 1; i++) { int swapflag = 0; for (int j = 0; j < arrSize - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapflag = 1; } } if (swapflag == 0) return ; } }
void bubbleSort2(int *arr, int arrSize) { for (int i = 0; i < arrSize - 1; i++) { int swapflag = 0; for (int j = arrSize - 1; j > i; --j) { if (arr[j] < arr[j - 1]) { int temp = arr[j]; arr[j] = arr[j - 1]; arr[j - 1] = temp; swapflag = 1; } } if (swapflag == 0) return ; } }
|
复杂度分析
- 时间复杂度: \(O(n^2)\);
- 最好情况下时间复杂度为 \(O(n)\);
- 初始序列有序,第一趟冒泡排序后直接跳出循环。此时比较次数为 \(n - 1\),移动次数为 \(0\);
- 最坏情况下时间复杂度为 \(O(n^2)\) ;
- 初始序列逆序,要进行 \((n - 1)\)
趟冒泡。第 \(i\) 趟要进行 \(n - i\) 次比较,移动次数为 \(3(n - i)\) ;
- 空间复杂度: \(O(1)\);
- 算法稳定性:稳定
5.快速排序
代码实现
int partition(int *arr, int low, int high) { int pivot = arr[low]; while (low < high) { while (low < high && arr[high] >= pivot) high--; arr[low] = arr[high]; while (low < high && arr[low] <= pivot) low++; arr[high] = arr[low]; } arr[low] = pivot; return low; } void quickSort(int *arr, int low, int high) { if (low >= high) return ; int mid = partition(arr, low, high); quickSort(arr, low, mid - 1); quickSort(arr, mid + 1, high); }
|
复杂度分析
- 时间复杂度: \(O(log_2n)\);
- 快速排序的运行时间与划分是否对称有关。
- 最坏情况:\(O(n^2)\);
- 两个区域分别包括 \(n - 1\) 和 \(0\) 个元素时,即序列开始时便逆序
- 最好情况:\(O(nlog_2n)\);
- 空间复杂度:\(O(nlog_2n)\);
- 由于快排是递归的,需要借助一个递归工作栈来保存每层递归调用的必要信息,其容量与递归调用的最大深度一致
- 最好的情况:\(O(log_2n)\);
- 最坏的情况:\(O(n)\);
- 算法稳定性:不稳定
- 在划分算法中,若右端区间有两个关键字相同,且均小于枢轴元素,则在交换到左端区间后,它们的相对位置会发生变化
选择排序
选择排序的基本思想是:每一趟(如第 \(i\) 趟)在后面 \(n - i + 1(i = 1, 2, ··· ,n - 1)\)
个待排序元素中选取关键字最小的元素,作为有序子序列的第 \(i\) 个元素,直到第 \(n - 1\)
趟排序做完,待排序元素只剩下1个,就不再选了。
6.简单选择排序
代码实现
void selectSort(int *arr, int arrSize) { for (int i = 0; i < arrSize; i++) { int minIndex = i; for (int j = i + 1; j < arrSize; j++) { if (arr[j] < arr[minIndex]) minIndex = j; } if (minIndex != i) { int temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } } }
|
复杂度分析
- 时间复杂度: \(O(n^2)\);
- 主要分为比较次数和移动次数
- 最坏情况下,比较次数为\((1 + 2 + ··· + n -
1)\), 移动次数为\(3(n -
1)\);
- 最好情况下,比较次数为\((1 + 2 + ··· + n -
1)\), 移动次数为 \(0\);
- 空间复杂度: \(O(1)\);
- 算法稳定性:不稳定
- {2,2, 1} 经过一趟排序后变为
{1,2, 2};
7.堆排序
代码实现
void adjustDown(int *arr, int arrSize, int k) { int tmp = arr[k]; for (int i = 2 * k + 1; i < arrSize; i = 2 * k + 1) { if (i + 1 < arrSize && arr[i] < arr[i + 1]) i++; if (tmp >= arr[i]) break; else { arr[k] = arr[i]; k = i; } } arr[k] = tmp; }
void buildMaxHeap(int *arr, int arrSize) { int i = arrSize / 2 - 1; for (; i >= 0; i--) { adjustDown(arr, arrSize, i); } }
void heapsort(int *arr, int arrSize) { buildMaxHeap(arr, arrSize); for (int i = arrSize - 1; i > 0; i--) { int temp = arr[0]; arr[0] = arr[i]; arr[i] = temp; adjustDown(arr, i, 0); } }
|
复杂度分析
- 时间复杂度: \(O(nlog_2n)\);
- 分为建立大根堆和调整堆
- 在建n个元素的堆时,时间复杂度为 \(O(n)\);
- 需要进行 \(n - 1\)
次堆调整,每次调整的时间复杂度为 \(O(h)\)
- 空间复杂度:\(O(1)\);
- 算法稳定性:不稳定
8.归并排序
代码实现
void merge(int *arr, int low, int mid, int high) { int *tmp = (int *)malloc(sizeof(int) * (high - low + 1)); int i = low, j = mid + 1; int k = 0; while (i <= mid && j <= high) { if (arr[i] < arr[j]) tmp[k++] = arr[i++]; else tmp[k++] = arr[j++]; } while (i <= mid) tmp[k++] = arr[i++]; while (j <= high) tmp[k++] = arr[j++]; for (int i = 0; i < k; i++) arr[low + i] = tmp[i]; } void mergesort(int *arr, int left, int right) { if (left >= right) return ; int mid = left + (right - left) / 2; mergesort(arr, left, mid); mergesort(arr, mid + 1, right); merge(arr, left, mid, right); }
|
复杂度分析
- 时间复杂度: \(O(nlog_2n)\)
- 每趟归并的时间复杂度为 \(O(n)\),需要进行 \(log_2n(向上取整)\) 趟归并
- 空间复杂度: \(O(n)\);
- merge()操作中,辅助空间刚好为\(n\)个单元
- 算法稳定性: 稳定
9. 计数排序
主要思想:开辟一个长为 maxVal - minVal +
1的计数数组,然后遍历一遍原数组,将元素值-minVal作为计数数组的索引,值则自增。
此时计数数组中不为0的元素值为原数组中元素值等于索引的个数。遍历计数数组,遇到不为0的元素,反写回原数组。
代码实现
void countSort(int *arr, int arrSize) { int maxVal = arr[0], minVal = arr[0]; for (int i = 1; i < arrSize; i++) { if (arr[i] > maxVal) maxVal = arr[i]; if (arr[i] < minVal) minVal = arr[i]; } int countArrSize = maxVal - minVal + 1; int *countArr = (int *)malloc(sizeof(int) * (countArrSize)); for (int i = 0; i < countArrSize; i++) countArr[i] = 0; for (int i = 0; i < arrSize; i++) { countArr[arr[i] - minVal]++; } int j = 0; for (int i = 0; i < countArrSize; i++) { while (countArr[i]) { arr[j++] = i + minVal; countArr[i]--; } } free(countArr); }
void countSort2(int *arr, int arrSize) { int maxVal = arr[0]; int minVal = arr[0]; for (int i = 1; i < arrSize; i++) { if (arr[i] > maxVal) maxVal = arr[i]; if (arr[i] < minVal) minVal = arr[i]; } int countArrSize = maxVal - minVal + 1; int *countArr = (int *)malloc(sizeof(int) * countArrSize); memset(countArr, 0, sizeof(int) * countArrSize); for (int i = 0; i < arrSize; i++) { countArr[arr[i] - minVal]++; } for (int i = 1; i < countArrSize; i++) { countArr[i] += countArr[i - 1]; } int *result = (int *)malloc(sizeof(int) * arrSize); for (int i = arrSize - 1; i >= 0; i--) { result[countArr[arr[i] - minVal] - 1] = arr[i]; countArr[arr[i] - minVal]--; } for (int i = 0; i < arrSize; i++) { arr[i] = result[i]; } }
|
复杂度分析
- 时间复杂度: $O(n + k) \(,\)k$ 为整数范围
- 最后的两层循环,最外层执行的次数为 \(k\), 最里层最终次数为\(n\), 因此总的次数是 \(n + k\);
- 空间复杂度:
- \(O(k)\),\(k\) 为整数范围(不稳定算法)
- \(O(k + n)\), (稳定算法)
- 算法稳定性:稳定 / 不稳定
10. 桶排序
主要思想就是:将要排序的数据分到几个有序的桶里,每个桶里的元素再单独进行排序。桶内排序完之后,再把每个桶里面的数据按照顺序依次取出,组成的序列就是排好序的序列。
要先确定好桶的个数和映射函数
代码实现
#include <stdio.h> #include <stdlib.h>
struct Bucket { int count; int* values; };
void bucketSort(int array[], int size) { int bucketCount = 10; struct Bucket buckets[bucketCount]; for (int i = 0; i < bucketCount; i++) { buckets[i].count = 0; buckets[i].values = (int*)malloc(sizeof(int) * size); } for (int i = 0; i < size; i++) { int bucketIndex = array[i] / bucketCount; buckets[bucketIndex].values[buckets[bucketIndex].count++] = array[i]; } for (int i = 0; i < bucketCount; i++) { if (buckets[i].count > 0) { for (int j = 0; j < buckets[i].count - 1; j++) { for (int k = 0; k < buckets[i].count - j - 1; k++) { if (buckets[i].values[k] > buckets[i].values[k + 1]) { int temp = buckets[i].values[k]; buckets[i].values[k] = buckets[i].values[k + 1]; buckets[i].values[k + 1] = temp; } } } } } int index = 0; for (int i = 0; i < bucketCount; i++) { for (int j = 0; j < buckets[i].count; j++) { array[index++] = buckets[i].values[j]; } free(buckets[i].values); } }
|
复杂度分析
- 时间复杂度: \(O(n)\)
- 空间复杂度:较高,具体取决于桶的数量和分配的空间
- 算法稳定性:取决于桶内排序的稳定性
- 桶排序仅适用于元素数据分布相对均匀且元素范围较小。
11.基数排序
主要思想是:第一趟排序的基数为1,即根据个位数放入相应的桶中,然后再将按桶号将桶中元素分别放回原数组,完成一趟排序,此时个位数已经有序
第二趟排序的基数为10,即根据10位数放入相应的桶中,重复以上过程。
趟数为最大元素的数字个数。
代码实现
int radixSort(int *arr, int arrSize) { int bucket[10] = {0}; int *result = (int *)malloc(sizeof(int) * arrSize); int maxVal = arr[0]; for (int i = 1; i < arrSize; i++) { if (arr[i] > maxVal) maxVal = arr[i]; } int maxValCount = 0; while (maxVal) { maxValCount += 1; maxVal /= 10; } int base = 1; for (int k = 0; k < maxValCount; k++) { for (int i = 0; i < arrSize; i++) { bucket[(arr[i] / base) % 10]++; } for (int i = 1; i < 10; i++) { bucket[i] += bucket[i - 1]; } for (int i = arrSize - 1; i >= 0; i--) { result[bucket[(arr[i] / base) % 10] - 1] = arr[i]; bucket[(arr[i] / base) % 10]--; } for (int i = 0; i < arrSize; i++) { arr[i] = result[i]; } for (int i = 0; i < 10; i++) { bucket[i] = 0; } base *= 10; } }
|
复杂度分析
- 时间复杂度: \(O(d(n +
r)\)
- \(d\) ——
最大数字的位数,即循环的次数
- \(r\) ——
数字的基数,表明桶的个数
- 空间复杂度:\(O(n +
r)\)
- 算法稳定性:稳定