总体的介绍
排序算法是非常重要也非常基础的算法,可以分为简单选择排序、直接插入排序、冒泡排序、希尔排序、堆排序、快速排序,归并排序,基数排序等。c++内置的sort()函数是集成了多种排序的方法,会按照输入的数组的规模选择不同的在测试中表现得更好得排序算法,所以一般来说它的效果是优于单一的某个排序算法的。对于面试,我们应该着重熟练的掌握快速排序,其他排序算法需要能够手撕。
Nlog(N)常用的排序算法
快速排序
快速排序也是交换排序的一种,其基本原理是:将未排序的元素根据一个作为基准的主元分为两个子序列,其中一个子序列均大于主元,另一个子序列均小于主元,然后递归的对于这两个子序列做自顶而下的递归。在这里主元的 选取有多种,通常为了避免最糟糕情况(也就是你选取的主元是当前数组的最大值或者最小值,这样的话你只能得到一侧的子序列,即要么均大于的,要么均小于的,也就没办法降低复杂度),所以一般是采取取给定数组的开始值,中间值,末尾值的中间值(也可以不做这一步处理),后续附上完整的代码。同时对于排序的问题,要小心区间的开闭,这里我们约定左闭右开的二分写法。平均时间复杂度为Nlog(N),不稳定(也就是说,当数组中有值相同时,可能会改变前后位置关系)。
//取主元的函数,选择数组第一个,最后一个,中间的三者中值
int choose_key(vector<int>&nums,int l,int r)
{
int center = (l+r-1)/2;
if(nums[l]>nums[center]) swap(nums[l],nums[center]);
if(nums[l]>nums[r-1]) swap(nums[l],nums[r-1]);
if(nums[center]>nums[r-1]) swap(nums[center],nums[r-1]);
//此刻有nums[l]<=nums[cnter]<=nums[r-1]
//将主元放置在第一位
swap(nums[center],nums[l]);
return nums[l];
}
void quick_sort(vector<int>&nums,int l,int r)
{
//边界值处理 当只有一个元素的时候
if(l+1>=r) return;
int first = l, last = r-1;
//第一种选主元的方案:直接选取第一个
//int key = nums[first];
//第二种选主元的方案:选取数组的第一个,最后一个,中间的元素,并且把选择的主元放置在数组的第一个,这样是便于存储,程序应该养成一种规范的意识
int key = choose_key(nums,l,r);
//打破循环的条件就是first == last
while(first<last)
{
while(first<last&&nums[last]>=key)
{
--last;
}
while(first<last&&nums[first]<=key)
{
++first;
}
//停下来的位置一定是小于主元的位置因为last在前
//如果满足左右序列的分布,没有越界的情况
swap(nums[first],nums[last]);
}
//这一步非常关键,一定要最后把主元放到两个子序列的中间进行分割,切记!!!
swap(nums[first],nums[l]);
quick_sort(nums,l,first);
quick_sort(nums,first+1,r);
}
归并排序
归并排序是建立在归并操作上的一种排序算法。其中归并排序是指将两个已经排序的子序列合并成一个有序序列的过程。
基本原理是:将大小为N的序列看作是N个长度为1的子序列,接下来将相邻的子序列两两进行归并操作,有点像双链表按照大小关系进行整合的题。具体的流程,简单来讲就是先使左序列有序化,再使右序列有序化,最后进行两个序列的归并。还记着我们之前左闭右开的约定吗?这里最后提醒一次,后面未经说明,我们均默认遵守这个约定。(归并排序需要额外的N空间复杂度来存储值)
void merge_sort(vector<int>&nums,int l,int r,vector<int>&record)
{
//不要在内部开辟新的临时空间,因为这样非常消耗栈空间
//处理边界条件
if(l+1>=r) return;
//分为两个序列
int m = l + (r-l)/2;
merge_sort(nums,l,m,record);
merge_sort(nums,m,r,record);
//归并操作
int p = l, q = m, cur = l;
while(p<m||q<r)
{
if(q>=r||(p<m&&nums[p]<=nums[q]))
{
record[cur++] = nums[p++];
}
else
{
record[cur++] = nums[q++];
}
}
//将归并好的部分赋值到原数组空间
for(i = l;i < r;++i)
{
nums[i] = record[i];
}
}
堆排序
主要是利用最大堆或者最小堆来进行插入排序,这里主要是采用内部容器。
#include<queue>
#include<vector>
std::priority_queue<int> heap; // 构造一个默认最大堆
std::priority_queue<int, std::vector<int>, std::greater<int> > small_heap; //构造一个最小堆
heap.top(); //返回最大或者最小元素。即根结点的值
heap.pop(); //删除最大或者最小元素,即根结点的值
heap.push(); //push一个值进最大/最小堆,根据值的大小排列堆
heap.empty(); //返回是否为空
heap.size(); //返回堆中元素个数
N2,Nd复杂度的常用排序算法
简答选择排序
排序思想:在未排序的序列当中选出最小的元素和序列的首位元素交换,接下来在未排序的序列中再找出最小的元素和第二个元素交换。
void simple_select(vector<int>&nums)
{
int n = nums.size(),min;
for(int i = 0;i<n;++i)
{
min = i;
for(int j = i+1;j<n;++j)
{
if(nums[j]<min) min =j;
}
swap(nums[min],nums[i]);
}
}
简单插入排序
核心思想:将待排序的一组序列分为已排序的和未排序的两部分,初始状态时,已排序部分仅包含数组的第一个元素,未排序部分为N-1,之后不断的将排序数组和未排序数组的界限向后推移直至整个数组完全排好序。
void insert_sort(vector<int>&nums)
{
int n = nums.size(),j;
for(int i = 1;i<n;++i)
{
int tmp = nums[i];//取出未排序序列的第一个元素
//依次和已经排序的元素相比较后右移
for(j = i;j>0&&nums[j-1]>tmp;--j)
{
nums[j] = nums[j-1];
}
//找到了插入的位置
nums[j] = tmp;
}
}
希尔排序
简单插入排序效率不高的原因就是每一次交换只能消除一个错位因子,而希尔排序对于插入排序进行改进,示图每次交换相隔一i的那个距离的元素。达到排序效率上的提升。
基本原理:将待排序的一组元素按一定间隔分为若干个序列,分别进行插入排序。开始设置的间隔较大,而后者在每轮的排序中逐步减少间隔,直到间隔为1,也就是最后一步时进行简单插入排序。
void shell_sort(vector<int>&nums)
{
int si,tmp,i;
int n = nums.size();
//这里随机设置一个增量序列,在尾部设置一个0作为哨兵减少判断步骤
vector<int>addnums{929,505,209,109,41,19,5,1,0};
//增量序列不应该超过数组本身的大小
for(si=0;addnums[si]>=n;++si);
//每次间隔一定的增量序列进行插入排序
for(int d = addnums[si];d>0;d = addnums[++si])
{
for(int q = d;q < n;++q)
{
tmp = nums[q];
for(i = q;i>=d&&nums[i-d]>tmp;i -= d) nums[i] = nums[i-d];
nums[i] = tmp;
}
}
}
冒泡排序
冒泡排序是最简单的交换排序。每一次都是大小数的浮动,在不断进行排序的过程中,大数和小数不断交换,让人感觉慢慢的小数就会向上浮动。
void bubble_sort(vector<int>&nums)
{
int n = nums.size();
for(int i = 0;i<n;++i)
{
int flag = true;//标记整个交换是否出现,如果遍历一遍没有出现交换的操作,说明已经排序完成
for(int j = n-1;j>i;--j)
{
if(nums[j]<nums[j-1])
{
swap(nums[j],nums[j-1]);
flag = false;
}
}
if(flag) break;
}
}
特殊的一种排序-桶排序
桶排序
思想:如果已知N个关键字的取值范围是0到M-1,而M比N要小的多(比如针对性的对于小写字符串,大写字符串,扑克牌等的存储排序),可以为关键字的每个可能的取值建立一个桶,在扫描N个关键字时,将关键字放到对应的桶中,而后再按照桶的顺序遍历一遍就自然有序了。
基数排序
桶排序的一种推广,主要是考虑不止一个关键字,就是会出现不同性质的桶。所以会出现对于关键字的优先度的定义,分别为主位优先和次位优先。