前言
排序在程序中非常常见,许多时候人们过于关注各种高级算法,在长久的工作学习中甚至只会调用库函数排序。闲来无事,整理了几种自己认为最常见的排序算法,仅为温习和备忘。
插入排序
插入排序的思想就是对每个待排序元素,找到其正确地插入位置。该元素左边是有序数组,右边是无序的。在左边找到该元素的插入位置,完成插入后,对下一个元素位置继续排序。
#include<vector>
using namespace std;
void insert_sort(vector<int> &nums)
{
int temp;
//遍历数组,对所有元素排序
for(auto i = 1; i < nums.size(); ++i)
{
temp = nums[i];
int j = i - 1;
//针对每个待排元素,遍历其左边有序序列找到插入位置,寻找的同时完成移位
while (j >= 0 && nums[j] > temp)
{
nums[j+1] = nums[j];
j--;
}
nums[j+1] = temp;
}
}
可见,插入排序是原地的,其最坏时间复杂度为O(n*n),空间复杂度O(1),插入排序是稳定的。当原数组是有序的时候,每次一进入内层循环就退出,时间复杂度可以提升到O(n),而当原数组刚好是逆序的时候,时间复杂度为O(n*n),且每一次都要发生一次移动。由此可见,当数据已经基本有序的时候使用插入排序是一个较好的选择。
我们发现对每个待排元素总要去其左边的有序序列寻找正确的插入位置,能不能对这一步提速呢,类似使用二分查找?其实是不行的,因为从代码可见,我们在寻找正确插入位置的时候还在移动元素,即使使用二分查找快速找到了插入位置,但还是要花费O(n)的时间移位。
shell排序
如果原来序列是逆序的,那么插入排序的性能会变得很差,针对这一点D.L.Shell提出了shell排序。
其思想为:首先将序列按照一个增量值分成很多组,每一组内进行插入排序。然后缩短增量,继续上述步骤,直到增量为1,这样的排序方法可以有效减少序列中的逆序对,提高性能。
#include<vector>
using namespace std;
void shell_sort(vector<int> &nums)
{
for(int delta = nums.size()/2; delta > 0; delta /=2)
{
for(int i = 0; i < delta; ++i)
{
ModInsSort(&nums[i], nums.size()-i; delta);
}
}
}
void ModInsSort(int *p, n, delta)
{
for(int j = delta; j < n; j += delta)
{
for(k = j; k >= delta; k -= delta)
{
if (*(p+k) < *(p+k-delta))
{
int temp = *(p+k);
*(p+k) = *(p+k-delta);
*(p+k-delta) = temp;
}
}
}
}
上述代码中ModInsSort()函数是插入排序的修改版。
shell排序是不稳定的,其最坏时间复杂度仍是O(n*n),空间复杂度为O(1),其具体表现与增量的选择有很大关系,其基本思想就是先通过几轮排序使得之后排序的序列基本有序,而在基本有序的情况下,插入排序是比较快的。关于其增量如何选择,有很多学者做了相关研究,在此不赘述。
选择排序
选择排序可以理解为,每次选择一个位置i,然后选择第i小的数据来占有这个位置即将两个位置的数据互换。
#include<vector>
using namespace std;
void select_sort(vector<int> &nums)
{
for(int i = 0; i < nums.size(); ++i)
{
int mini = i;
for(int j = i; j<nums.size(); ++j)
{
if (nums[j] < nums[mini])
{
mini = j;
}
}
int temp = nums[i];
nums[i] = nums[mini];
nums[mini] = temp;
}
}
选择排序是不稳定的,最坏、最好和平均时间复杂度都为O(n*n),空间复杂度O(1)
堆排序
堆排序基本步骤为,构造一个最大堆,然后把最大元素和堆的最后一个元素互换,然后调整堆和堆的大小
#include<vector>
using namespace std;
//移除堆顶元素,至堆的末尾,每次结束重新调整堆
void remove_max(vector<int> &nums, int currentsize)
{
swap(nums[0], nums[currentsize - 1]);
currentsize--;
shftdown(nums, 0, currentsize);
}
//向下调整函数。若子节点存在比父节点大的,将父节点与子节点中较大的交换。直到不存在,就调整完毕。
void shiftdown(vector<int> &nums, int i, int currentsize)
{
int large = 2*i + 1;
int temp = nums[i];
while (large < currentsize)
{
if ((large < currentsize - 1) && (nums[large+1] > nums[large]))
{
large += 1;
}
if (nums[large] > temp)
{
nums[i] = nums[large];
i = large;
large = 2*i + 1;
}
else
break;
}
nums[i] = temp;
}
//建立最大堆,建堆的过程是针对每个非叶子节点进行shiftdown即向下调整
void build_heap(vector<int> &nums, maxsize)
{
for (int i = maxsize/2 - 1; i >= 0; --i)
{
shiftdown(nums, i, maxsize)
}
}
//堆排序的主要步骤。1、建立最大堆。2、每次移除堆顶元素,直到堆空
void heap_sort(vector<int> &nums)
{
int max_size = nums.size();
build_heap(nums, maxsize);
for (int i = maxsize; i > 0; --i)
{
remove_max(nums, i);
}
}
堆排序最好,最坏和平均时间复杂度都是O(nlogn),空间复杂度O(1),是不稳定的。由于一次建堆就可以反复使用,因此n越大效率越高。
快速排序
快速排序算法是二十世纪十大算法之一,可以认为是“最快”的算法。其基本的算法思想是分治,归并排序基本思想也是分支,但是不同的是,快速排序重点在分,而归并重点在合。
快排的基本步骤为:1、选定一个轴值。2、将比轴值小的数移到左边,比轴值大的数移到右边,则此刻轴值的位置就正确了。3、针对轴值两边的数组继续上述步骤,直到只剩一个数据。
#include<vector>
using namespace std;
void quick_sort(vector<int> &nums, left, right)
{
if (left >= right) return;
int pivot = (left + right)/2;
int l = left, r = right;
int temp = nums[pivot];
nums[pivot] = nums[r];
while (l != r)
{
while (nums[l] <= temp)
{
++l;
}
if (l < r)
{
nums[r] = nums[l];
--r;
}
while (nums[r] >= temp)
{
--r;
}
if (l < r)
{
nums[l] = nums[r];
++l;
}
}
nums[l] = temp;
quicksort(nums, left, l-1);
quicksort(nums, l+1, right);
}
快排平均、最小时间复杂度是O(nlogn),最坏时间复杂度是O(n*n),空间复杂度是O(1),是不稳定排序。快排是最常用的排序算法,虽然说现在各种排序算法都可以直接调用库函数,但是能快速手写这种经典算法也是有必要的。
归并排序
前面已经提到过了,归并排序也是用的分治法的思想。但是“分,治,合”中,归并在意的是合的过程,将两个有序数组合并为一个数组,只需要一遍扫描即可。归并排序的步骤为1、选一个中间值将数组分为两半。2、重复上述步骤知道两边只剩一个数。3、此时两边都有序,将两边数组合并
#include<vector>
using namespace std;
void merge(vector<int> &nums, vector<int> &temp, int left, int right, int mid)
{
for(int i = left; i <= right; ++i)
{
temp[i] = nums[i];
}
int index1 = mid + 1;
int index2 = left;
int j = left;
while ((index1 <= right) && (index2 <= mid))
{
if (nums[index1] < temp[index2])
{
nums[j] = nums[index1]
j++;
index1++;
}
else
{
nums[j] = temp[index2]
j++;
index2++;
}
}
while (index1 <= right)
{
nums[j] = nums[index1];
j++;
index1++;
}
while (index2 <= mid)
{
nums[j] = temp[index1];
j++;
index2++;
}
}
void merge_sort(vector<int> &nums, vector<int> &temp, left, right)
{
int mid = (left + right)/2;
if (left < right)
{
merge_sort(nums, temp, left, mid);
merge_sort(nums, temp, mid+1, right);
merge(nums, temp, left, right, mid);
}
}
归并排序最好、最坏、平均时间复杂度都是O(nlogn),空间复杂度O(n),是稳定的。其中temp数组是用来临时存放用的,其实也可以不从外部传入,在merge()函数内部建立,建立的时候只要n/2大小即可,但并没有什么数量级的提高。可以自己根据情况自由选择
冒泡排序
冒泡排序可以算是我接触的最早的计算机程序了,那还是初中的时候。。冒泡排序非常好写,基本思想就是依次把最大的数移到数组的末尾,就像一个气泡冒出来一样。
#include<vector>
using namespace std;
void bubble_sort(vector<int> &nums)
{
for (int i = nums.size() - 1; i > 0; --i)
{
for (int j = 0; j < i; ++j)
{
if (nums[j] > nums[j+1])
{
swap(nums[j], nums[j+1]);
}
}
}
}
冒泡排序时间复杂度O(n*n),空间复杂度O(1)。平时不要使用。。
小结
这是几种最常用的内排序算法,虽然很底层,但还是有必要了解。特别是快速排序,掌握它应该是基本素养。而且对于一般情况,各种实验验证,快速排序的确是最快的排序算法。