一、各大排序算法
PS:默认升序排列
(1)冒泡排序
基本思路:类似鱼吐泡泡,从左往右依次比较相邻两个元素,更大数换到右边。第一轮将最大数冒到最后,第二轮将次大数冒到倒数第二个位置,以此类推,直到排序完整个数组。
//冒泡排序:先将最大数冒到最后,再将次大数冒到倒数第二个位置,以此类推
void bubbleSort(vector<int> &nums) {
int n = nums.size();
for (int i = 0; i < n; i++) {//外层循环: 保证每个数都被排序
for (int j = 1; j < n-i; j++) {//内层循环: 比较后冒泡,排序终点前移
if (nums[j-1] > nums[j]) {
swap(nums[j-1], nums[j]);//将更大数冒到后面
}
}
}
}
**一个简单的优化(优化效果有限):若在循环过程中发现剩余待排数组元素已经按非降序排列好,则跳出循环,结束排序。用于当循环执行到某一时刻,若发现左侧未排序数组已升序排列,则后续循环不用执行。
//冒泡排序优化:若在循环过程中发现待排数组元素已经按非降序排列好,则跳出循环
void bubbleSort1(vector<int> &nums) {
int n = nums.size();
for (int i = 0; i < n; i++) {//外层循环 保证每个数都被排序
bool flag = true;
for (int j = 1; j < n-i; j++) {//内层循环 比较后冒泡,排序终点前移
if (nums[j-1] > nums[j]) {
swap(nums[j-1], nums[j]);//将更大数冒到后面
flag = false;//只要有前比后大的情况,待排数组就不是已经排列好
}
}
if (flag)
break;//若某次循环时没有前大于后的情况,说明待排数组本身非降序,结束循环
}
}
(2)插入排序
基本思路:类似扑克牌的插牌过程,将某张牌插入到已排好序的牌组内的正确位置处。
将排序模拟成向有序数组中插入新元素的过程(初始时刻将待排序数组的第一个元素想象成一个只有一个元素有序数组),只需找到新元素应该插入的位置,依次插入即可。
根据执行插入的方式不同,直接插入排序有两种实现方式——
(a)以交换操作来表示插入这一过程。将待插入的新元素从右往左依次与有序数组内的元素进行比较,若新元素更小,交换其与比较元素的位置(使小数在左,大数在右),直到左边的数更大,说明新元素已插入到正确位置,退出循环,开始插入下一个新元素。
//直接插入排序1:以交换操作来表示插入这一过程
void insertSort1(vector<int> &nums) {
int n = nums.size();
//初始时,把第一个元素看作一个有序数组
for (int i = 1; i < n; i++) {
int num = nums[i];//待插入的新元素
for (int j = i-1; j >=0; j--) {
if (num < nums[j]) {
//由于左侧为有序数组,若num更大,无需插入
//若num更小,交换操作实际上是将num值左移一位,直到前一位比它小为止
swap(nums[j], nums[j+1]);
}
else break;
}
}
}
(b) 后移+插入。更形象地模仿插扑克牌的过程,将新元素与有序数组从右往左的元素依次比较,只要有序数组内的元素更大,就将其后移一位。直到比较元素小于新元素,即找到新元素的插入位置,将该元素插入,完成一轮排序,开始插入下一个待排序的新元素。
//直接插入排序2:向左侧的有序数组中插入新元素。后移+插入
void insertSort2(vector<int> &nums) {
int n = nums.size();
//初始时,把第一个元素看作一个有序数组
for (int i = 1; i < n; i++) {
int num = nums[i];//待插入的新元素
int j = i-1;//先定义j,否则循环后将不能使用j
while (j >= 0) {
if (num < nums[j]) {
nums[j+1] = nums[j];//将有序数组中大的值向后移一位
j--;
}
else break; //因左侧数组是有序的,不满足条件时可以跳出整个内层循环
}
nums[j+1] = num;//插入num值到合适位置(j+1是因为前面的循环会让j多前移一位
}
}
(3)希尔排序
基本思路:在数组内从下标0开始,每隔gap个下标取一个数放入同一小组,最终将数组分为gap个小组(同一小组内每个元素的下标均相隔gap);分别对每个小组执行插入排序,使数组相对有序;最后对整体再执行一次插入排序。效率比直接插入排序更高。
分组举例:
设一个数组:{1,3,2,5,7,8,3,9}
gap = 3;
分组结果:{1,5,3}; {3,7,9};{2,8}
分组间隔 gap 的选取:考虑到插入排序的排序原理,其对接近有序的数组排序效率更高。希尔排序对每个小组执行插入排序,因而每个小组越有序越好,这与分组间隔gap的取值有关。
gap越大,每小组内数越少,组内较大的数能更快地排到后面;
gap越小,预排出的数组越有序(gap=1时即为直接插入排序)。
因此,gap的值不能固定, 要随数组长度n变化。且gap的初值为n,终值为1(最后要对整体执行一次插入排序)。
gap常用的两种取值方式为:gap = gap/2 和 gap = gap/3+1
//希尔排序:将数组分为gap个小组(同一小组内每个元素的下标相隔gap),分别对每个小组插入排序,使数组相对有序;最后对整体插入排序,效率比直接插入排序更高
void shellSort(vector<int> &nums) {
int n = nums.size();
int gap = n;//gap的初值为数组大小
while (gap > 1) {
gap = gap/3 + 1;//保证最后一次gap=1;也可以gap = gap/2,前者预排序数组更有序
//对每个小组内元素插入排序
for (int i = gap; i < n; i++) {
int num = nums[i];
int j = i - gap;
while (j >= 0) {
if (nums[j] > num) {
nums[j+gap] = nums[j];//更大数向后移
j -= gap;
}
else
break;//前小于后时,结束内层循环
}
nums[j+gap] = num;//将num插入合适位置(j+gap因为j一直指向待插入的前一个)
}
}
}
(4)选择排序
基本思路:从数组内选择最小的数放到左侧排序起点处,排序起点依次右移。
先假设nums[0]最小(首轮排序起点),遍历右侧元素,找到最小值与nums[0]交换;排序起点右移一位到nums[1],遍历右侧元素,找到本轮的最小值(即整体的次小值)与nums[1]交换;以此类推,排序完整个数组。( “选择” 体现在寻找每轮最小值的过程不改变数组元素值,只记录下标,每轮结束后交换一次)
//选择排序:选择最小的数放到左侧排序起点(选择过程不改变数组元素,只记录索引)
void selectionSort(vector<int> &nums) {
int n = nums.size();
for (int i = 0; i < n-1; i++) {//外层循环到n-1时,再经过一次内层循环,排序即可结束
int minIndex = i;//先假设i处元素值最小
for (int j = i+1; j < n; j++) {
if (nums[j] < nums[minIndex]) {
minIndex = j;
}
}
swap(nums[i], nums[minIndex]);//找到本次循环的最小数后,与排序起点交换
}
}
(5)堆排序
堆:可看作一颗完全二叉树的数组对象,满足:
① 堆中父节点的值总是不小于或不大于其子节点;
② 堆总是一颗完全二叉树。
完全二叉树的性质: