目录
一、三种简单的初级排序算法(冒泡、选择、直接插入)
1.冒泡排序
每个数和自己隔壁的数两两对比,就像冒泡一样,右边开始有序,时间复杂度为O(n^2)
#include<iostream>
using namespace std;
int main()
{
int a[100];
int n;
cin>>n;
int i,j;
for(i=0;i<n;i++)
cin>>a[i];
int tmp;
for(i=0;i<n-1;i++)
for(j=0;j<n-i-1;j++)
{
if(a[j]>a[j+1])
{
tmp=a[j];
a[j]=a[j+1];
a[j+1]=tmp;
}
}
for(i=0;i<n;i++)
cout<<a[i]<<" ";
return 0;
}
改进的冒泡排序(引入flag):
和上面唯一的差别在于加入了一个flag,这个改进主要适用于减少诸如
0 -1 1 2 3 4 5 6
这种顺序的序列的比较次数,即左边一部分无序,右边一部分有序。
我们把左边称为无序区,把右边称为有序区。
第一次的第一层循环之后其实后面已经不用再比较了,加入了flag,就不用再进入第二层循环执行判断。
在第二层循环中,元素有交换,则说明数列无序;如果没有元素交换,说明数列已然有序,直接跳出大循环。
好好理解一下是不是这样,第二层可以遍历到无序区每一个未被排序的数,如果第二层循环中没有元素交换,一定是整个数列有序了。
时间复杂度为O(n^2),这里其实不知道我有没有算错。
#include<iostream>
using namespace std;
int main()
{
int a[100];
int n;
cin>>n;
int i,j;
for(i=0;i<n;i++)
cin>>a[i];
int tmp;
int flag=1;
for(i=0;i<n-1&&flag;i++)
{
flag=0;
for(j=0;j<n-i-1;j++)
{
if(a[j]>a[j+1])
{
tmp=a[j];
a[j]=a[j+1];
a[j+1]=tmp;
flag = 1; //有数据交换,说明此次第一层循环没排好序,下次第一层循环有可能需要排序,flag设为1。
}
}
}
for(i=0;i<n;i++)
cout<<a[i]<<" ";
return 0;
}
改进的冒泡排序(对数列有序区的界定):
对不起对不起我太懒了,偷了一张参考资料博客里面的图
大概就是这个意思,到后面的循环里面,右边的数据已经是有序了,但是还是白白比较了,
所以现在的核心就是要对数列有序区进行界定。
按照之前的逻辑,有序区的长度和排序的轮数是相等的。比如第一轮排序过后的有序区长度是1,第二轮排序过后的有序区长度是2。
实际上,数列真正的有序区可能会大于这个长度,比如例子中仅仅第二轮,后面5个元素实际都已经属于有序区。因此后面的许多次元素比较是没有意义的。
如何避免这种情况呢?我们可以在每一轮排序的最后,记录下最后一次元素交换的位置,那个位置也就是无序数列的边界,再往后就是有序区了。
这下面的代码中,增加了border变量和lastExchangeIndex 变量
border就是无序数列的边界。每一轮排序过程中,border之后的元素就完全不需要比较了,肯定是有序的。
#include<iostream>
using namespace std;
int main()
{
int a[100];
int n;
cin>>n;
int i,j;
for(i=0;i<n;i++)
cin>>a[i];
int tmp;
int flag=1;
int border = n-1;
int lastExchangeIndex = 0;
for(i=0;i<n-1&&flag;i++)
{
flag=0;
for(j=0;j<border;j++)
{
if(a[j]>a[j+1])
{
tmp=a[j];
a[j]=a[j+1];
a[j+1]=tmp;
flag = 1; //有数据交换,说明此次第一层循环没排好序,下次第一层循环有可能需要排序,flag设为1。
lastExchangeIndex = j;
}
}
border = lastExchangeIndex;
}
for(i=0;i<n;i++)
cout<<a[i]<<" ";
return 0;
}
2.选择排序
每次选择一个最小的,和最左边的交换,左边开始有序。时间复杂度为O(n^2)
#include<iostream>
using namespace std;
void swap(int *i,int *j)
{
int tmp;
tmp = *i;
*i=*j;
*j=tmp;
}
int main()
{
int a[5]={9,8,99,66,2};
int i,j;
int min;
int n=5;
for(i=0;i<n-1;i++)
{
min = i;
for(j=i+1;j<n;j++)
{
if(a[j]<a[min])
{
min = j;
}
}
if(min != i)
swap(&a[i],&a[min]);
}
for (i=0;i < 5;i++)
cout<<a[i]<<" ";
cout<<endl;
return 0;
}
3.直接插入排序
类似于打扑克牌!!!
左边当成有序,右边当成无序,一开始默认第一个元素处在有序的序列中,之后每次从右边无序的序列中选出第一个,以冒泡的方式,插入到左边的有序序列中。时间复杂度为O(n^2)
#include<iostream>
using namespace std;
void swap(int *i,int *j)
{
int tmp;
tmp = *i;
*i=*j;
*j=tmp;
}
int main()
{
int a[5]={9,8,99,66,2};
int i,j;
int n=5;
for(i=0;i<n-1;i++)
{
for(j=i+1;j>0;j--)
{
if(a[j]<a[j-1])
{
swap(&a[j],&a[j-1]);
}
}
}
for (i=0;i < 5;i++)
cout<<a[i]<<" ";
cout<<endl;
return 0;
}
二、希尔排序
参考资料:
https://www.cnblogs.com/chengxiao/p/6104371.html
希尔排序其实是一种改进的插入排序,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。
对于大规模乱序数组,插入排序很慢,因为它只会交换相邻的元素,因此元素只能一点一点地从数组的一端移动到另一端。假如,如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要N-1次移动。希尔排序为了加快速度简单的改进了插入排序,交换不相邻的元素对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
我们来看下希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2...1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
#include<iostream>
using namespace std;
void swap(int *i,int *j)
{
int tmp;
tmp = *i;
*i=*j;
*j=tmp;
}
int main()
{
int a[10]={8,9,1,7,2,3,5,4,6,0};
int i,j;
int n=10;
int gap;
for(i=0;i<n;i++)
cout<<a[i]<<" ";
cout<<endl;
for(gap = n/2;gap>0;gap = gap/2) //第一层循环,步长gap逐渐减小。
{
for(i=0;i<n-gap;i++) //第二层循环,这里和插入排序有点不一样,这么写可以把所有情况都遍历到。
{
for(j=i+gap;j-gap>=0;j=j-gap) //第三次循环,根据步长,“跳着”进行插入排序的对比及交换。
{
if(a[j]<a[j-gap])
{
swap(&a[j],&a[j-gap]);
}
}
}
}
for (i=0;i < n;i++)
cout<<a[i]<<" ";
cout<<endl;
return 0;
}
三、快速排序
很多企业面试都会问快排,这是一个比较重要的排序算法。
快速排序是基于二分的思想,对冒泡排序的一种改进
快速排序基本思想:
通过一趟排序将要排序的数据分割成独立的两部分:分割点左边都是比它小的数,右边都是比它大的数。然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序原理:第一步:设置两个指针left和right分别指向数组的头部和尾部,并且以头部的元素(6)为基准数
第二步:right指针先往左移动,找到小于基准数的元素就停下,然后移动left指针(想一下为什么是right先移动,不能是left先移动)
第三步:left指针往左移动,找到大于基准数的元素就停下,此时两个指针都停下来了,然后交换right和left指针所值元素的值
重复第二、三步,直到两个指针left和right重合
第四步:两个指针重合后将基准数(6)与两个指针指向的元素值(3)交换
先回答第二步为什么是right先移动,不能是left先移动的问题。
假设现在第一轮排序进入到最后一步,指针left和指针right已经是邻居,下一步要见面了,
假设基准数选为数列第一个数a[0],值为6,a[left] = 5,a[right] = 7
现在的排序情况是 6 x x x x 5 7 n n n n n
其中x都小于6,n都大于6,下一步肯定要移动指针了
1.如果先移动right指针:
两个指针在数值为5的地方见面,按照算法,将5和基准值6互换位置,皆大欢喜,排序正常。
2.如果先移动left指针:
两个指针在数值为7的地方见面,按照算法,将7和基准值6互换位置,这个时候7在数列的最开头位置,比基准值6大的数,居然出现在6的左边,明显算法失效了。
新版本的笔记,可以直接看这个:
快速排序code注意事项:
1.两个函数,一个挖坑填数findPivot(),一个递归分治quickSort()
2.findPivot有一个while循环嵌套两个子while循环,条件都是left < right
3.findPivot的两个子while记得break
4.findPivot最后记得pValue赋值给轴点元素
5.quickSort也得加判断left<right
为什么快排中最开始要从右边的right开始往左边遍历?
因为我们的轴点元素选择的是最左边的值,挖坑填数嘛,right找到一个小于轴点元素的,就会把这个值赋值给轴点元素所在的位置,就是这样。
为什么快排中元素和索引元素值相同的时候,也需要换位置呢?
因为如果不这么做,容易造成分割出来的两个子序列不均匀,极端情况(比如序列所有元素相等)就是一个序列被分割成长度为n和长度为0的两个子序列,造成算法复杂度变成O(n)。
简单来说就是交换的好处在于可以把序列分割成两个长度均匀的子序列。
#include<iostream>
using namespace std;
//快排第一版
//挖坑填数
int findPivot(int s[],int left,int right){
int pValue;
pValue = s[left];
while(left<right){
while(left<right){ //从右边元素开始遍历
if(s[right] > pValue){ //右边元素大于轴点元素
right--;
}
else{ //右边元素小于轴点元素
s[left] = s[right];
left++; //先填数,然后left元素位置右移
break; //保证执行上面两条挖坑填数语句之后,换方向遍历,也就是让left开始游走,执行下面的while
}
}
while(left<right){
if(s[left]<pValue){
left++;
}
else{
s[right] = s[left];
right--;
break; //保证执行上面两条挖坑填数语句之后,换方向遍历,也就是让right开始游走,执行上面的while
}
}
}
s[right] = pValue; //也可以写成s[left] = pValue;
return right;
}
//递归分治
void quickSort(int s[],int left,int right){
if(right - left <= 0){ //这里之前写的是<2,也就是<=1,是写错了,我是看那个教程写的,现在发现根本不是这么一回事。
return;
}
/* //也可以写成下面这个:
if(right <= left){ //注意这里是right<=left,不是left<right,可以才考v2和v3的代码,
//在v2和v3中,我们会发现只有left<right的时候才能进入递归,
//所以right<=left就表示不进入递归,而是要逃出递归,进行return。
return;
}*/
int pivot = findPivot(s,left,right);
quickSort(s,left,pivot-1);
quickSort(s,pivot+1,right);
}
int main()
{
int s[10] = {2,3,5,4,1,6,9,8,7,10};
//int s[2] = {2,1};
int n = 10;
for(int i=0;i<n;i++){
cout<<s[i];
}
cout<<endl;
quickSort(s,0,n-1);
for(int i=0;i<n;i++){
cout<<s[i];
}
return 0;
}
四、堆排序
创建三个函数:
1.heapify(int tree[],int n,int i)
创建以 i 为节点的堆。
2.buildHeap(int tree[],int n)
以第一个非叶子节点开始,从右向左,从下到上,创建大顶堆。
3.heapSort(int tree[],int n)
前面两个函数构建好堆的结构,开始把堆的首尾数组元素交换,进行排序。
#include<iostream>
using namespace std;
//堆排序,从最后一个非叶子节点往上构建.
void swap(int s[],int i,int j)
{
int tmp;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
//错误1:if(c1 < n && tree[c1] > tree[maxIndex] ) 写成了 if(c1 < n && tree[c1] > tree[i] )
//错误2: if(c1 < n 不是 if(c1 <= n ,注意看哈
//错误3:buildHeap(tree,n) 没有放在heapSort()函数里面
//错误4:heapSort()函数的for循环里面,应该是heapify(tree,i,0),而不是heapify(tree,n,0);
void heapify(int tree[],int n,int i){
if(i >= n) return ;
int maxIndex = i; //最大索引元素
int c1 = 2*i + 1; //左子树的索引
int c2 = 2*i + 2; //右子树的索引
if(c1 < n && tree[c1] > tree[maxIndex] ){ //如果左子树大于节点,则认为最大值的索引是左子树的索引
maxIndex = c1;
}
if(c2 < n && tree[c2] > tree[maxIndex]){ //如果右子树大于节点,则认为最大值的索引是右子树的索引
maxIndex = c2;
}
if(maxIndex != i){
swap(tree,maxIndex,i);
heapify(tree,n,maxIndex);
}
}
void buildHeap(int tree[],int n){ //从最后一个非叶子节点开始构建初始的大顶堆。
int lastNode = n-1;
int parentNode = (lastNode - 1)/2;
int i;
for( i=parentNode; i>=0; i--){
heapify(tree,n,i);
}
}
void heapSort(int tree[],int n){
buildHeap(tree,n);
int i;
for( i=n-1; i>=0; i--){
swap(tree,i,0);
heapify(tree,i,0);
}
}
int main()
{
int tree[]={9,8,99,66,2,7,54,45,32,76,83,26};
int i,j;
int n=12;
heapSort(tree,12);
for (i=0;i < 12;i++)
cout<<tree[i]<<" ";
cout<<endl;
return 0;
}
什么是排序算法稳定性?
稳定排序是指原来相等的两个元素前后相对位置在排序后依然不变。
为什么要追求排序算法的稳定性?
例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序。
假设本来输入的序列是 2 3 3 4 5 1,这个序列是已经按照价格排好序的,现在要按照销量排序,要保证输出的结果中,第一个3依然在第二个3前面。
参考文献: