七大经典排序
插入排序
插入排序开始先把第一个数字作为一个有序子数组,然后从第二个数字开始。
既然前面是一个有序的数组,那么当前这个数组只要逐个跟前面的有序子数组比较就行。
如果当前数字比前面的数字小,就把前面的数字往后挪一个位置。
直到当前的数字比比较的数字( 下标为:j )大为止,把当前数字填入到 j + 1 的位置。
即可表示当前数字插入成功。
// 插入排序
//
// 时间复杂度
// a. 最好:O(N) 已经有序的情况
// b. 平均:O(N^2)
// c. 最坏:O(N^2) 已经有序是逆序
//
// 空间复杂度
// O(1)
//
// 稳定性:稳定
void InsertSort(std::vector<int>& arr)
{
// 外循环实现减治过程
// 一次取一个数,插入到前面的有序区间里
for (size_t i = 1; i < arr.size(); i++)
{
int key = arr[i];
// 内部的循环实现的是插入的过程
// j 从 [i - 1, 0]
// 如果 arr[j] > key, 往后搬
// 如果 arr[j] == key, 跳出循环(保证了稳定性)
// 如果 arr[j] < key, 跳出循环
int j;
for (j = i - 1; j >= 0 && arr[j] > key; j--)
{
arr[j + 1] = arr[j];
}
// j + 1 就是要插入的位置
arr[j + 1] = key;
}
}
希尔排序
希尔排序是在插入排序的基础上做了一个优化。
希尔排序是把一个数组先分成若干组,先对这几组的数据,每组之间进行插入排序。
然后,缩小间隔(gap),也就是说,减少组的数量,再次进行插入排序。
这样做的目的就是为了在最后一次,只有一组的情况下,尽可能的让这个数组里的数字已经有序。
gap 的变化是 :
先让 gap == arr.size()
然后 gap = gap / 3 + 1
再进行排序
直到 gap == 1 的情况排完,说明,这个数组已经完成了排序。(gap == 1, 说明它把整个数组分成了一个组,进行插入排序,排完一定有序),就可以跳出了。
// 希尔排序
// 希尔排序是插入排序的优化版本
// 在插入排序之前,尽可能的让数据有序
// 分组插排
// 通过维护一个 gap 来实现分组
// gap 初始化 为 gap = arr.size() / 3 + 1
// 后面 gap = gap / 3 + 1
//
// 时间复杂度
// a. 最好: O(N)
// b. 平均: O(N^1.2) ~ O(N^1.3)
// c. 最坏: O(N^2) 这里虽然跟插入排序的最坏时间复杂度一样
// 可是它把遇到最坏情况的概率变小了
//
// 空间复杂度
// O(1)
//
// 稳定性:不稳定。
// 一旦分组,很难保证相同的数在一个组里
// 可以设置 gap 间隔的插入排序
void InsertSortWithGap(std::vector<int>& arr, int gap)
{
for (size_t i = gap; i < arr.size(); i++)
{
int key = arr[i];
int j;
for (j = i - gap; j >= 0 && arr[j] > key; j -= gap)
{
arr[j + 1] = arr[j];
}
// j + 1 就是要插入的位置
arr[j + gap] = key;
}
}
// 希尔排序
void ShellSort(std::vector<int>& arr)
{
int gap = arr.size();
while (true)
{
gap = gap / 3 + 1;
InsertSortWithGap(arr, gap);
// 如果 gap == 1 ,说明上次排序已经排完了整个数组
if (gap == 1)
break;
}
}
堆排序
堆排序比较好理解。
如果排升序,首先建一个大堆。
然后每次把对顶元素(数组里最大的数字)和最后一个元素交换。这个时候向下调整的时候,size - 1 。也就是说,把最后一个元素进行向下调整,忽略已经在数组最后的堆顶元素。
以此类推,直到 size = 1 已经把所有数字排完。(就是依次把当前 size 个数组元素里最大的数字放到最后)
// 堆排序--》升序建大堆
//
// 时间复杂度
// 最好 = 平均 = 最坏 = O(N * LogN)
// 向下调整时间复杂度是 O(LogN)
// 建堆的时间复杂度是 O(N * LogN)
// 排序的时间复杂是 O(O * LogN)
//
// 空间复杂度 O(1)
//
// 稳定性:不稳定
// 向下调整过程中,无法保证相等数的前后关系
//
// 向下调整
void AdJustDown(std::vector<int>& arr, int size, int root)
{
// 如果此时 root 是叶子结点,调整结束退出
if (root * 2 + 1 >= size)
return;
// 找到最大的孩子
int left = (root * 2) + 1;
int max = left;
int right = (root * 2) + 2;
if (right < size && arr[right] > arr[left])
{
max = right;
}
// 判断当前 root 是否小于叶子结点
if (arr[root] < arr[max])
{
std::swap(arr[root], arr[max]);
AdJustDown(arr, size, max);
}
return;
}
// 建堆
void CreateHeap(std::vector<int>& arr)
{
// 从第一个双亲结点开始向下调整
// 逐渐向上走,直到根结点向下调整结束
for (int i = (int)(arr.size() - 2) / 2; i >= 0; i--)
{
AdJustDown(arr, (int)arr.size(), i);
}
}
// 堆排序
void HeapSort(std::vector<int>& arr)
{
// 先建堆
CreateHeap(arr);
// 然后每次将堆顶元素和最后一个元素交换
// size - 1
// 向下调整
for (int i = 0; i < (int)arr.size(); i++)
{
std::swap(arr[0], arr[arr.size() - i - 1]);
AdJustDown(arr, arr.size() - i - 1, 0);
}
}
选择排序
这个排序也很简单。
就是每次把当前数组里最大的元素放到最后。然后进行减治运算就行。
(优化版本也就是每次选两个,一个最大元素的下标,一个最小元素的下标。然后把最大的放到最后,最小的放到最前面,再进行减治)
// 选择排序
//
// 每次遍历无序区间,找到最大数的下标
// 1. 交换最大的数和无序区间的最后一个数
// 2. 区间 size - 1
// 3. 重新循环
// 时间复杂度
// 最好 = 最坏 = 平均 = O(N^2)
//
// 空间复杂度
// O(1)
//
// 稳定性:不稳定。
// {9, 4, 3, 5a, 5b} 无法保证5a在5b前
void SelectSort(std::vector<int>& arr)
{
for (int i = 0; i < (int)arr.size() - 1; i++)
{
int max = 0;
for (int j = 0; j < (int)arr.size() - i; j++)
{
if (arr[j] > arr[max])
max = j;
}
std::swap(arr[max], arr[arr.size() - 1 - i]);
}
}
冒泡排序
这个跟选择排序差不多,但是冒泡排序是每次通过交换的方式,把最大的放到最后面。
然后进行减治。
// 冒泡排序
//
// 时间复杂度
// 最好 :O(N)
// 最坏|平均:O(N^2) 逆序
//
// 空间复杂度
// O(1)
//
// 稳定性:稳定
void BubbleSort(std::vector<int>& arr)
{
for (int i = 0; i < (int)arr.size(); i++)
{
int sorted = 0;
for (int j = 0; j < (int)arr.size() - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
std::swap(arr[j], arr[j + 1]);
sorted = 1;
}
}
if (sorted == 0)
break;
}
}
快速排序
快速排序是先确定一个基准值(一般取数组的最左边的数或者最右边的数)我这里取数组最右边的数,然后确定两个下标left right。
如果基准值取最右边的数,那么先让左侧的下标从左往右找,找到第一个比基准值大的数字停下来,然后右侧的下标开始从右往左遍历数组,找到第一个比基准值小的值停下来。交换 left right 所对应的值。
直到两个下标相遇,把相遇的数字和基准值交换就可以了。
然后以基准值为中心,把数组分成两个小数组,再次在小数组内进行刚才的操作。
直到小数组的元素个数小于等于1,就可以停下来了。
三种分割方法
1. hoare 法
// hoare 版本
int Parition(std::vector<int>& arr, int left, int right)
{
int div = right;
while (left < right)
{
// 比较的时候要加 = 的情况
// 比如说一个例子 1 1 1 1 1
// 如果不加等于 就成死循环了
// 先从左边找到一个比基准值大的数字
while (left < right && arr[left] <= arr[div])
left++;
// 从右边开始找一个比基准大小的数字
while (left < right && arr[right] >= arr[div])
right--;
if (left < right)
std::swap(arr[left], arr[right]);
}
std::swap(arr[left], arr[div]);
return left;
}
2. 挖坑法
// 挖坑法
int ParitionDiggintPit(std::vector<int>& arr, int left, int right)
{
// 把基准值获取到,然后这个位置成为了一个坑
// 下次找到满足条件的数,就把那个数填进去
// 那么被填进去的数原本的位置就成了新的坑
int base_value = arr[right];
int pit = right;
int div = right;
while (left < right)
{
while (left < right && arr[left] <= arr[div])
{
left++;
}
// 找到一个比基准值大的值,将这个值填入坑中
arr[pit] = arr[left];
// 更新坑的位置
pit = left;
while (left < right && arr[right] >= arr[div])
{
right--;
}
// 找到了一个比基准值小的值,将这个值填入坑中
arr[pit] = arr[right];
// 更新坑的位置
pit = right;
}
arr[pit] = base_value;
return pit;
}
3. 拉窗帘法
// 拉窗帘法
int ParitionSlideWindow(std::vector<int>& arr, int left, int right)
{
int div = right;
int d = 0;
int c = 0;
while (c < div)
{
// 始终让滑动窗口内部的值大于基准值
if (arr[c] >= arr[div])
{
c++;
}
else
{
if (d < c)
std::swap(arr[d], arr[c]);
d++;
c++;
}
}
// 走到这表示 d 之前的值都比基准值小
// 把基准值和 arr[d] 交换即可
std::swap(arr[d], arr[div]);
return d;
}
整体代码:
// 快速排序
// [left, right]
// 1. 在要排序的区间内选择一个基准值
// 具体方法:
// 1)选择区间最右边这个数 arr[right]
//
// 2. 遍历整个区间,做一些数据交换,达到效果:
// 比基准值小的数,放到基准值左侧
// 比基准值大的数,放到基准值右侧
//
// 3. 分治算法:把一个问题变成两个同样的小问题
//
// 4. 用递归或非递归
// 终止条件:
// 1)分出来的小区间没有数了:分出的区间 size == 0
// 2)分出来的小区间已经有序了:区间的 size == 1
// 时间复杂度
// 最好|平均:O(N * LogN)
// 遍历一遍数组:O(N)
// 高度: Log N
//
// 最差:O(N^2)
// 如果已经有序,就是一个单支树。
//
// 空间复杂度
// O(LogN) ~ O(N)
// 递归调用的深度(二叉树的高度)
//
// 稳定性:不稳定
// Parition 三种方式
// hoare 版本
int Parition(std::vector<int>& arr, int left, int right)
{
int div = right;
while (left < right)
{
// 比较的时候要加 = 的情况
// 比如说一个例子 1 1 1 1 1
// 如果不加等于 就成死循环了
// 先从左边找到一个比基准值大的数字
while (left < right && arr[left] <= arr[div])
left++;
// 从右边开始找一个比基准大小的数字
while (left < right && arr[right] >= arr[div])
right--;
if (left < right)
std::swap(arr[left], arr[right]);
}
std::swap(arr[left], arr[div]);
return left;
}
// 挖坑法
int ParitionDiggintPit(std::vector<int>& arr, int left, int right)
{
int base_value = arr[right];
int pit = right;
int div = right;
while (left < right)
{
while (left < right && arr[left] <= arr[div])
{
left++;
}
// 找到一个比基准值大的值,将这个值填入坑中
arr[pit] = arr[left];
// 更新坑的位置
pit = left;
while (left < right && arr[right] >= arr[div])
{
right--;
}
// 找到了一个比基准值小的值,将这个值填入坑中
arr[pit] = arr[right];
// 更新坑的位置
pit = right;
}
arr[pit] = base_value;
return pit;
}
// 拉窗帘法
int ParitionSlideWindow(std::vector<int>& arr, int left, int right)
{
int div = right;
int d = 0;
int c = 0;
while (c < div)
{
// 始终让滑动窗口内部的值大于基准值
if (arr[c] >= arr[div])
{
c++;
}
else
{
if (d < c)
std::swap(arr[d], arr[c]);
d++;
c++;
}
}
// 走到这表示 d 之前的值都比基准值小
// 把基准值和 arr[d] 交换即可
std::swap(arr[d], arr[div]);
return d;
}
void _QuickSort(std::vector<int>& arr, int left, int right)
{
// 终止条件
if (left >= right)
return;
int div; // 基准值取最右边的值
div = ParitionSlideWindow(arr, left, right); // 遍历 arr[left, right],把小的放左,大的放右
_QuickSort(arr, left, div - 1);
_QuickSort(arr, div + 1, right);
}
void QuickSort(std::vector<int>& arr)
{
_QuickSort(arr, 0, (int)arr.size() - 1);
}
归并排序
这个排序比较有意思。
现在说一个特殊的例子。如何给两个有序的数组进行排序呢?就是先开辟一个空间足以容纳两个数组的数组,然后分别从两个数组的开头遍历,谁小就往里放就行了。
那么归并也是这意思。它是先把一个数组一直二分。直到分成最小数组(指的是数组里只有一个或者两个元素),然后两个两个的进行合并。
然后逐渐向上回溯,两个两个的数组越来越大的而已。最后进行的就是合并两个有序数组。(这两个数组各占一半需要排序的数组)。
// 归并排序
// 对一个大数组进行排序的问题
// 变成了对左右两个小数组进行排序的问题o
//
// 时间复杂度
// 最好|平均|最坏:O(N * LogN)
//
// 空间复杂度
// O(N)
//
// 稳定性:稳定
//
// 优点:可以对硬盘上的数据进行排序
// 合并两个有序数组
// arr[left, mid)
// arr[mid, right)
// 该操作的
// 时间复杂度
// O(N)
//
// 空间复杂度
// O(N)
void Merge(std::vector<int>& arr, int left, int mid, int right,
{
int left_index = left;
int right_index = mid;
int index = 0;
while (left_index < mid && right_index < right)
{
if (arr[left_index] <= arr[right_index]) // <= 之所以加
{
tmp[index++] = arr[left_index++];
}
else
{
tmp[index++] = arr[right_index++];
}
}
while (left_index < mid)
{
tmp[index++] = arr[left_index++];
}
while (right_index < right)
{
tmp[index++] = arr[right_index++];
}
for (int i = 0; i < right - left; i++)
{
arr[left + i] = tmp[i];
}
}
void _MergeSort(std::vector<int>& arr, int left, int right, std:
{
if (right == left + 1)// 区间内还剩一个数
{
return;
}
if (left >= right)// 区间内没有数了
{
return;
}
int mid = left + (right - left) / 2;
// 区间被分成左右两个小区间
// [left, mid)
// [mid, right)
// 先把左右两个小区间进行排序,分治算法,递归解决
_MergeSort(arr, 0, mid, tmp);
_MergeSort(arr, mid, right, tmp);
// 左右两个小区间已经有序
// 合并两个有序数组
Merge(arr, left, mid, right, tmp);
}
void MergeSort(std::vector<int>& arr)
{
std::vector<int> tmp(arr.size());
_MergeSort(arr, 0, arr.size(), tmp);
}
总结:
插入排序:
时间复杂度(平均):O(N^2)
空间复杂度:O(1)
稳定性:稳定
希尔排序:
时间复杂度:O(N^1.2) ~O(N^1.3)
空间复杂度:O(1)
稳定性:不稳定
选择排序:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:不稳定
冒泡排序:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
堆排序:
时间复杂度:O(N*LogN)
空间复杂度:O(1)
稳定性:不稳定
快速排序:
时间复杂度:O(N*LogN)
空间复杂度:O(LogN) ~ O(N)
稳定性:不稳定
归并排序:
时间复杂度:O(N*LogN)
空间复杂度:O(N)
稳定性:稳定