摘要:
本文是对常见的排序算法进行总结和分析,也是自己在找工作经常被问到的问题的一些总结。
排序算法稳定性的定义:
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的
相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
1.1冒泡排序的思想
对相邻的元素进行两两比较,顺序相反则进行交换。这样每一趟将最小或最大的元素浮到顶端。最终达到完全有序。
参考链接:
冒泡排序
1.2常见问题
冒泡排序时间复杂度是?
冒泡排序最坏的时间复杂度是O(n^2)。
为什么?考虑最坏的情况,完全倒序,那么需要比较的次数就是n+(n-1)+(n-2)+....1,等差数列求和O(n^2)
如何优化?
优化的思想其实就是添加一个标志,若在某一趟比较中没有发生交换,则停止后面的比较。这样在(最佳)顺序排列的时候时间复杂度O(n),也就是检查一轮就结束排序。
冒泡排序是否稳定?
稳定,但是冒泡排序也可以转化为不稳定算法。
即将比较的条件 if (a[i]>a[i+1]) ====> if (a[i]>=a[i+1]) 转化为不稳定排序算法。
1.3冒泡排序代码
void f(int a[],int n)
{
int i,j,flag=1,temp
for (i=1;i<n&&flag==1;i++)
{
flag=0
for (j=0;j<n-i;j++)
{
if (a[j]>a[j+1])
{
flag=1
temp=a[j]
a[j]=a[j+1]
a[j+1]=temp
}
}
}
return a
}
2.快排
2.1快速排序的思想
1.选择一个基数。
2.分区,把小于基数的排左边,大于基数的排右边。
3.对得到两个分区重复以上步骤,直至分区只有1个元素
比如:
i=0 | j=4 | |||
5 | 4 | 1 | 7 | 2 |
i=0 | j=4 | |||
5 | 4 | 1 | 7 | 2 |
i=0 | j=4 | |||
2 | 4 | 1 | 7 | 2 |
i=3 | j=4 | |||
2 | 4 | 1 | 7 | 2 |
=>改变位置
i=3 | j=4 | |||
2 | 4 | 1 | 7 | 7 |
重复上述步骤,直到移动到i=j时,基数已经把左边变成都比基数小的,右边都比基数大的,就可以用分别对左右两边重复上述步骤。
最后基数要归位(i=j)
=>基数归位(i=j)
i=3,j=3 | ||||
2 | 4 | 1 | 5 | 7 |
然后对 :
i=0 | j=2 | |
2 | 4 | 1 |
j=4 |
7 |
2.2快排的时间和空间的复杂度?
快排最坏的情况是O(n^2),顺序或者逆序都是。最佳的情况就是O(nlogn)。
(注意:所谓最坏其实就是分治的时候分成两个极不平衡的数组,比如n个元素分成一个是n-1个元素和一个1个元素的.)
快排的空间复杂度最坏的情况下是O(n),通常情况下为O(logn)
2.3如何优化快排?
方法一:不要总把数组第一个数选择基数,采取随机选择。
方法二:三数取中。比方说有序列: 8 1 4 9 6 3 5 2 7 0
取最左边、最最右边以及中间的。分别是8 0 6。取三个数中间的数即 0 6 8 的6。
把取到的数和序列第一个数交换,也就是得到序列: 6 1 4 9 8 3 5 2 7 0,继续进行快排。
其实方法一、方法二都是针对基数的选择来进行优化。
2.4 快排的核心代码
其实快排的核心代码是下面这一部分:
while(i<j)
{
while(i<j && a[j]>=temp)
j--;
a[i]=a[j];
while(i<j && a[i]<=temp)
i++;
a[j]=a[i];
}
a[i]=temp;
2.5 快排递归完整代码
void sort(int a[],int left,int right)
{
if (left >= right)
{return; }
int i,j,temp;
i=left;
j=right;
temp=a[i];
while( i<j)
{
while(a[j]>=temp && i<j)
{
j--;
}
a[i]=a[j];
while(a[i]<=temp && i<j)
{
i++;
}
a[j]=a[i];
}
a[i]=temp;
sort(a,left,i-1);
sort(a,i+1,right);
}
2.5 快排非递归完整代码
void sort(a,left,right)
{
if (left<right)
{
int mid=partition(a,left,right)
if (left+1<mid)
{
st.push(left)
st.push(mid-1)
}
if (right-1>mid)
{
st.push(mid+1)
st.push(right)
}
}
while(!st.empty())
{
int right=st.top()
st.pop()
int left=st.top()
st.pop()
int mid=partition(a,left,right)
if (left+1<mid)
{
st.push(left)
st.push(mid-1)
}
if (right-1>mid)
{
st.push(mid+1)
st.push(right)
}
}
}
partition就是上面提到的核心代码。
非递归的思想其实就是利用一个栈来存放4个索引(left,mid-1)和(mid+1,right)。
3.归并排序
3.1 归并排序原理
具体可以看看博客:
https://www.cnblogs.com/chengxiao/p/6194356.html
原理大概是两个阶段:
阶段1是分。分的时候就是把整个数组分成只有一个元素的数组。
阶段2是合。就是两两合并。合并的时候利用两个哨兵i,j。分别指向两个集合。然后比较-移动,看看核心代码就明白。
3.2 归并时间与空间复杂度
归并排序的时间复杂度为O(nlogn),最坏也是O(nlogn).是一种稳定的算法
归并排序的空间复杂为O(n),这里主要是由在merge时候产生一个O(n)的辅助数组决定的.
3.3 归并排序核心代码
归并排序的核心代码。
首先是要想到两个集合的哨兵应该是:
int i=low;
int j=mid+1;
之后就是比较两个集合哨兵位置的元素大小
if (list[i]>=list[j])
{
temp[k++]=list[j++];
}
else
{
temp[k++]=list[i++];
}
再补上循环的条件,循环条件就应该是两个集合的最末端。
while (i<=mid && j<=high)
{
if (list[i]>=list[j])
{
temp[k++]=list[j++];
}
else
{
temp[k++]=list[i++];
}
}
最后,肯定有一个集合有剩。就需要把元素都取完
while( i<=mid )
{
temp[k++]=list[i++];
}
while( j<=high)
{
temp[k++]=list[j++];
}
之后就把temp粘回原来的list中,开始结束的终点分别是low,high
for(i=low,k=0;i<=high;i++,k++)
{
list[i]=temp[k];
}
void split(int list[],int left,int right)
{
if (left<right)
{
int mid=(left+right)/2;
split(list,left,mid);
split(list,mid+1,right);
merge(list,left,mid,right);
}
4.堆排序
堆排其实就是升序的时候用大顶堆(最大值在根节点),降序用小顶堆(最小值在根节点)。
而大顶堆或者小顶堆其实是一个完全二叉树,只不过这颗完全二叉树还满足以下性质:
任意一个父节点的值大于或者等于其左右孩子节点的值。
即有:a[i]>=a[i*2+1] 且a[i]>=a[i*2+2]。(根节点的编码为0,如果编码为1的话就是a[i]>=a[i*2] 且a[i]>=a[i*2+1])
所以!所以!任意节点的左右子树也是大顶堆。但是!但是!但是!对左右孩子节点的值没有任何要求。
这样的完全二叉树就是一个大顶堆(类似有小顶堆)
另外,完全二叉树可以变现为一个一维的数组。比如:
使用大顶堆来排序的步骤其实很简单,只需要两部分。
第一部分:建堆。
第二部分:把要排序的元素放到堆顶。
建堆思路:
第一步:
找到第一个非叶子节点。这个非叶子节点的编号为len/2-1。(len是待排序列的长度,而且注意节点编码从0开始,也就是说根节点编号为0。如果根节点编号从1开始的化就是len/2。而且这里取整的。)
比如,上面图的第一个非叶子节点是(5/2-1)=(2-1)=1。编码为1的节点为第一个非叶子节点。
为啥是len/2-1呢。简单解释一下。
len是整个数组长度,也是完全二叉树里面编号最大的一个值。编号最大的这个值所在的这一层肯定是最后一层,其上一层就是非叶子节点所在的第一层。len/2就是上一层其中一个节点的编号。
第二步:
从第一个非叶子节点开始,比较其与左右孩子节点值的大小。如果父节点值小于孩子节点的值,那么需要把两者交换。
注意注意注意!!!交换之后,要对交换后要以被交换的孩子节点为根节点建立一个大顶堆,其实就是递归的过程。
第三步:
叶子节点的编号不断 -1 其实就是先左移到最左边,然后移到上一层的最右边。即从右->左。
最后可以建立好一个大顶堆。
注意注意注意!!这个大顶堆一定有任意的父节点值大于或者等于其左右孩子节点值。
上一部分把大顶堆建好之后就可以用大顶堆来排序了。
注意注意!!!上面提到大顶堆其实是一个一维数组,但是即使已经是大顶堆了,这个数组也是无序的。只不过这个数组的第一个元素是最大的!!!!!
所以,建好堆之后的排序每次其实就是筛选出次最大值!!!!!!!
具体做法:
每次把堆尾a[len-1]的元素移动到堆顶a[0]来,并且把堆的长度-1,然后重新建堆,但是这次建堆不是完全的重新建立,而是一个调整。
思路看完,还是得看代码,代码一步一步撸:
首先,我们核心是要比较父节点和左右孩子节点的大小:
if (a[max]<a[left] && left<=heapsize ){
max=left;
}
if (a[max]<a[right] && right<=heapsize){
max=right;
}
而左右孩子节点的编号由关系有:
int left =i*2+1;
int right=i*2+2;
int max=i;
max用来存放最大的节点。初始化成i就可以了。
然后,当遇到左右孩子节点比父节点大的时候需要进一步判断被更换后的节点是否保持大顶堆的性质,比如:
左边的树,左孩子节点1比父节点0要大,要更换,更换后,节点[1,3,4]不满足大顶堆的性质,是节点1导致的,需要把节点1看做作根节点进行一次建大顶堆,具体来说代码:
if (i!=max)
{
int temp=0;
temp=a[i];
a[i]=a[max];
a[max]=a[i];
BUildheap(a,max,heapsize);
}
if用来判断是否父节点小于左(右)孩子节点的值。然后调用Buildheap,所以完整代码:
void Buildheap(int a[],int i,int heapsize):
{
int left =i*2+1;
int right=i*2+2;
int max=i ;
if (a[max]<a[left] && left<=heapsize ){
max=left;
}
if (a[max]<a[right] && right<=heapsize){
max=right;
}
if (i!=max)
{
int temp=0;
temp=a[i];
a[i]=a[max];
a[max]=temp;
Buildheap(a,max,heapsize)
}
}
然后,建立大顶堆从最右边第一个非叶子节点开始,然后不断上移(编号 -1)。
for (int i=len/2-1;i>=0;i--)
{
Buildheap(a,i,len-1);
}
len是待排序列长度。所以上面这个for循环就完成了建大顶堆的过程。
下面就是利用大顶堆来排序(其实就是筛选出最大值)
for (int i=len-1;i>0;i--)
{
int temp=0;
temp=a[0];
a[0]=a[i];
a[i]=temp;
Buildheap(a,0,i-1);
}
每次把堆顶元素放到数组最后,把数组最后的元素放到堆顶。
记得每次数组长度缩减1(i--)。
所以核心就是两个for循环。一个用于建大顶堆,一个把大顶堆用来筛选就次大值。用的都是同一个程序Buildheap()。
完整代码:
void Buildheap(int a[],int i,int heapsize)
{
int left =i*2+1;
int right=i*2+2;
int max=i ;
if (a[max]<a[left] && left<=heapsize ){
max=left;
}
if (a[max]<a[right] && right<=heapsize){
max=right;
}
if (i!=max)
{
int temp=0;
temp=a[i];
a[i]=a[max];
a[max]=temp;
Buildheap(a,max,heapsize);
}
}
void sort(int a[],int len)
{
for (int i=len/2-1;i>=0;i--)
{
Buildheap(a,i,len-1);
}
for (int i=len-1;i>0;i--)
{
int temp=0;
temp=a[0];
a[0]=a[i];
a[i]=temp;
Buildheap(a,0,i-1);
}
}
5.插入排序
插入排序的核心步骤是这样的对于一个待排的数组a
a[0]已经是排序好了
排序a[0:1]->排序a[0:2]->排序a[0:3]...一直样下去。
每次发现后面的元素a[i]小于前面的元素a[i-1]时,就把前面的元素往后挪。知道找到a[i]的位置。
代码:
void sort(vector<int > &a){
for(int i=1;i<a.size();i++){
if(a[i]<a[i-1])//后面比前面小才动
{
int temp=a[i];
int j=i;
while(j>=1 && a[j-1]>temp)
{
a[j]=a[j-1];
j--;
}
a[j]=temp;
}
}
}