排序算法
首先,这里的是否稳定评判标准如下:假设$a$在$b$的前面,且$a=b$,若排序之后,$a$和$b$的顺序没变,则该排序算法是稳定的,若$a$到了$b$的后面,则该排序算法不稳定。冒泡排序(优化)
基本思想:两两比较,如果反序则交换,每次冒泡,都会有一个元素到达最终的位置。
冒泡排序对n个数据操作n-1轮,每轮找出一个最大(小)值。
- 最好时间复杂度为 ∗ ∗ O ( n ) ∗ ∗ **O(n)** ∗∗O(n)∗∗:输入的数组刚好是顺序,比较n-1便,不需要交换操作。但需要采用优化后的代码。
- 最坏时间复杂度为 ∗ ∗ O ( n 2 ) ∗ ∗ **O(n^2)** ∗∗O(n2)∗∗:输入的数组完全逆序,每轮排序的每一次比较都要交换。
- 平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)
- 空间复杂度为 O ( 1 ) O(1) O(1):交换法,仅在交换时需要一个元素的额外空间。
- 稳定性:元素相同时不做交换,是稳定的排序算法。
代码如下:
void BubbleSort(vector<int>& arr) {
int n = arr.size();
if (n < 2) { return; }
for (int i = 0; i < n - 1; ++i) {
for (int j = 0; j < n - 1 - i; ++j) {
if (arr[j] > arr[j + 1]) {
swap(arr[j], arr[j + 1);
}
}
}
}
void BubbleSort(vector<int>& arr) {
//优化版,只有在优化版中,最好时间复杂度才是O(n)
int n = arr.size();
if (n < 2) { return; }
for (int i = 0; i < n - 1; ++i) {
bool did_swap = false;
for (int j = 0; j < n - 1 - i; ++j) {
if (arr[j] > arr[j + 1]) {
did_swap = true;
swap(arr[j], arr[j + 1);
}
}
if (!did_swap) { break; }
}
}
快速排序(单向遍历、双向遍历)
基本思想:通过一次排序将待排的记录分割成两个独立的部分,其中一部分记录的关键字均比另一部分的关键字小,则可以分别对这两部分记录继续进行排序,以达到整个序列有序。
- 从数列中调出一个元素,称为**“基准”**(pivot);
- 重新排序数组,所有元素比基准小的放在基准前面,所有元素比基准大的放在基准后面(相同的数可以到任意一边)。在这个分区退出之后,该基准就出于数组的中间位置,这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数组和大于基准值元素的子数组排序;
分析:
- 最好时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n):每次选择基准时刚好把数组分为平均的两部分,划分次数为 O ( l o g 2 n ) O(log_2n) O(log2n),每次划分比较一遍 O ( n ) O(n) O(n),时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。
- 最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2):每次选基准都选到了最大值或者最小值,那么就需要n次的分区操作,每次需要比较n次。
- 平均时间复杂度为 ∗ ∗ O ( n l o g 2 n ) ∗ ∗ **O(nlog_2n)** ∗∗O(nlog2n)∗∗:大部分情况下,很难选到极端情况。
- 空间复杂度:快排的空间复杂度和递归的深度有关,最坏情况下,需要n次的分区操作,所以递归要用到的栈的大小也为 O ( n ) O(n) O(n),平均情况为 O ( l o g 2 n ) O(log_2n) O(log2n)。
- 稳定性:快速排序的分区操作涉及到交换操作,是不稳定更多排序算法。
代码如下:
int Partition_in_Quick_1(vector<int>& arr, int left, int right) {
// 单向遍历
int pivot = arr[left], pos = left;
while (left < right) {
while (left < right && arr[right] >= pivot) {
--right;
}
while (left < right && arr[left] <= pivot) {
++left;
}
if (left < right) {
swap(arr[left], arr[right]);
}
}
arr[pos] = arr[left];
arr[left] = pivot;
return left;
}
int Partition_in_Quick_2(vector<int>& arr, int left, int right) {
// 双向遍历
int pivot = arr[left];
while (left < right) {
while (left < right && arr[right] >= pivot) {
--right;
}
arr[left] = arr[right];
while (left < right && arr[left] <= pivot) {
++left;
}
arr[right] = arr[left];
}
arr[left] = pivot;
return left;
}
void QuickSort(vector<int>& arr, int left, int right) {
if (left < right) {
int pos = Partition_in_Quick_1(arr, left, right);
// int pos = Partition_in_Quick_2(arr, left, right);
QuickSort(arr, left, pos - 1);
QuickSort(arr, pos + 1, right);
}
}
插入排序
插入排序(Insertion-Sort)是通过构建有序序列,对于未排序的数据,在已排序的序列中从后往前扫描,找到相应位置并插入。
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果扫描的的元素大于正在排序的元素,则将扫描到的元素移动到下一个位置;
- 重复上一步骤,直到扫描到已经排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置;
- 重复步骤2-5;
简单插入排序童谣操作n-1轮,每轮都将一个未排序的数据插入到已经排序的数据中。
- 最好时间复杂度为 O ( n ) O(n) O(n):当数组刚好顺序,每次只用一次比较即可,重复n-1次。
- 最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2):当数组完全逆序时,要比较1+2+3+…+n-1次才能完成整个排序。
- 平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)
- 空间复杂度为 O ( 1 ) O(1) O(1):仅在移位时需要过渡空间。
- 稳定性:元素相同时不做交换,是稳定的排序算法。
代码如下:
void InsertSort(vector<int>& arr) {
int n = arr.size();
if (n < 2) { return; }
for (int i = 1; i < n; ++i) {
int target = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > target) {
swap(arr[j], arr[j + 1]);
--j;
}
}
}
希尔排序
Shell Sort是简单插入排序的改进,又称缩小增量排序。它与插入排序的不同之处在于,它会优先比较距离较远的元素。
希尔排序将序列按固定间隔划分为多个子序列,在子序列中简单插入排序,先做远距离移动使序列基本有序;逐渐缩小间隔重复操作,最后间隔为1时即简单插入排序。
希尔排序的时间复杂度主要由其增量序列来决定,目前最好的增量序列下,希尔排序的时间复杂度大致为 O ( n 1.3 ) O(n^{1.3}) O(n1.3)。希尔排序的平均复杂度是否能达到 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)也是一个尚未解决的问题。
其空间复杂度也是 O ( 1 ) O(1) O(1)。
注意,由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。**
代码如下:
void ShellSort(vector<int>& arr) {
int n = arr.size();
if (n < 2) { return; }
int step = n >> 1;
while (step) {
for (int i = step; i < n; ++i) {
while (i >= step && arr[i] < arr[i - step]) {
swap(arr[i], arr[i - step]);
i -= step;
}
}
step >>= 1;
}
}
选择排序
Selection Sort是同样对数据操作n-1轮,每轮找出一个最小(大)值。以此类推,直到所有元素均排序完毕。
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。
时间复杂度均为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)。由于会交换顺序,所以选择排序是不稳定的。
代码如下:
void SelectSort(vector<int>& arr) {
int n = arr.size();
if (n < 2) { return; }
for (int i = 0; i < n - 1; ++i) {
int min_index = i;
for (int j = i + 1; j < n; ++j) {
min_index = (arr[j] < arr[min_index]) ? j : min_index;
}
if (i != min_index) {
swap(arr[i], arr[min_index]);
}
}
}
堆排序
HeapSort是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,其满足子节点的键值或索引总是小于(或者大于)它的父节点。排序过程如下:
- 构造一个大顶堆,取堆顶数字A[0](也就是当期最大值)与最后一个元素A[n-1]交换;
- 新的无序区(A[0]、A[1]、…、A[n-2])和新的有序区(A[n-1])。再将剩下的数字构建一个大顶堆,取堆顶数字(也就是剩下值当中的最大值)A[0]与无序区最后一个元素A[n-2]交换;
- 得到新的无序区(A[0]、A[1]、…、A[n-3])和新的有序区(A[n-2]、A[n-1]);
- 重复以上操作,直到取完堆中的数字;
堆排序的初始建堆过程恒比较复杂,要对 O ( n ) O(n) O(n)级别个非叶子节点进行堆调整操作 O ( l o g 2 n ) O(log_2n) O(log2n),时间复杂度即为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。之后每一次堆调整操作确定一个数的次序,时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。合起来时间复杂度即为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。
空间复杂度为 O ( 1 ) O(1) O(1),在调整堆的过程中需要暂存空间。
代码如下:
void HeapAdjust(vector<int>& arr, int i, int n) {
int largest = i, left = 2 * i + 1, right = 2 * (i + 1);
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]);
HeapAdjust(arr, largest, n);
}
}
void HeapSort(vector<int>& arr) {
int n = arr.size();
for (int i = n / 2; i >= 0; --i) {
HeapAdjust(arr, i, n);
}
for (int j = n - 1; j > 0; --j) {
swap(arr[0], arr[j]);
HeapAdjust(arr, 0, j);
}
}
归并排序(递归、非递归、辅助数组归并、原地归并)
归并排序的原理其实是分治法。将数组不断二分,直到最后每个部分只包含一个数据,然后再对每个部分分别进行排序,最后将排序号的相邻的两部分合并在一起。这就是2路归并。
其时间复杂度均为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。空间复杂度在使用辅助数组时为 O ( n ) O(n) O(n),不采用辅助数字时为 O ( 1 ) O(1) O(1)。
代码如下:
void Merge(vector<int>& arr, int left, int mid, int right) {
int n_temp = right - left + 1;
vector<int> temp(n_temp);
int t = 0; //辅助数组的起始下标
int i = left, j = mid + 1; //两个子序列的起始位置
while (i <= mid && j <= right) {
if (arr[i] < arr[j]) {
temp[t++] = arr[i++];
}else {
temp[t++] = arr[j++];
}
}
while (i <= mid) {
temp[t++] = arr[i++];
}
while (j <= right) {
temp[t++] = arr[j++];
}
for (int k = 0; k < n_temp; ++k) {
arr[left + k] = temp[k];
}
}
void _reverse(vector<int>& arr, int begin, int end) {
while (begin < end) {
swap(arr[begin++], arr[end--]);
}
}
void MergeInPlace(vector<int>& arr, int left, int mid, int right) {
// 原地归并,不需要辅助数组
int i = left, j = mid + 1;
while (i < j && j <= right) {
while (i < j && arr[i] <= arr[j]) {
++i;
}
int old_j = j;
while (j <= right && arr[i] > arr[j]) {
++j;
}
_reverse(arr, i, old_j - 1);
_reverse(arr, old_j, j - 1);
_reverse(arr, i, j - 1);
i += (j - old_j);
}
}
void MergeSort(vector<int>& arr, int left, int right) {
// 递归
if (left < right) {
int mid = left + ((right - left) >> 1);
//int mid = (left + right) / 2;
MergeSort(arr, left, mid);
MergeSort(arr, mid + 1, right);
Merge(arr, left, mid, right);
}
}
void MergeSort_Non_Recursive(vector<int>& arr) {
// 非递归
int n = arr.size(), cur_len = 1;
while (cur_len <= n) {
for (int i = 0; i <= n - cur_len; i += cur_len * 2) {
int left = i, mid = i + cur_len - 1, right = i + cur_len * 2 - 1;
right = (right >= n) ? n - 1 : right;
//Merge(arr, left, mid, right); // 辅助数组合并
MergeInPlace(arr, left, mid, right); // 原地合并
}
cur_len *= 2;
}
}
本文部分理论内容参考夏普通。