好像可以保研,但是最近没事也找了几份工作。看到各家的IT巨头的招聘题目,发现算法工程师中的机器学习方向很热,可自己在传统的数据结构还有算法分析方向还是一个很弱的孩子。现在更新一些hihocoder还有ACM的题目,对编程比较有帮助,语言用c++。后期会用java,有时候也用matlab的脚本或者是Python。重在算法本身。
今天突发奇想,看到了排序算法。于是想在这里总结一下大学时候学过的一些排序算法,具体代码放在自己的github主页上,时间还有空间复杂度。排序的时候是从小到大排序的。
这些文章的结构式首先讲各种排序的原理还有代码(C++,代码在linux上g++编译通过。需要的同学可以到我的github主要上下载,记得看reade)。之后会比较分析各种算法的时间复杂度。最后给大家介绍这些算法背后的深层次的原理-----------分治的实现
这些算法的如果想知道的很详细,建议看书,《数据结构预算法分析c语言版》或者是《算法导论》,这里只做实例理解。或者这篇博文
一.算法实例及代码
1.冒泡排序
我相信谷歌,谷歌的第一篇文章就是这样,于是就转载了这个大牛的一个实例。
原理是临近的数字两两进行比较,按照从小到大或者从大到小的顺序进行交换,
这样一趟过去后,最大或最小的数字被交换到了最后一位,
然后再从头开始进行两两比较交换,直到倒数第二位时结束,其余类似看例子
例子为从小到大排序,
原始待排序数组| 6 | 2 | 4 | 1 | 5 | 9 |
第一趟排序(外循环)
第一次两两比较6 > 2交换(内循环)
交换前状态| 6 | 2 | 4 | 1 | 5 | 9 |
交换后状态| 2 | 6 | 4 | 1 | 5 | 9 |
第二次两两比较,6 > 4交换
交换前状态| 2 | 6 | 4 | 1 | 5 | 9 |
交换后状态| 2 | 4 | 6 | 1 | 5 | 9 |
第三次两两比较,6 > 1交换
交换前状态| 2 | 4 | 6 | 1 | 5 | 9 |
交换后状态| 2 | 4 | 1 | 6 | 5 | 9 |
第四次两两比较,6 > 5交换
交换前状态| 2 | 4 | 1 | 6 | 5 | 9 |
交换后状态| 2 | 4 | 1 | 5 | 6 | 9 |
第五次两两比较,6 < 9不交换
交换前状态| 2 | 4 | 1 | 5 | 6 | 9 |
交换后状态| 2 | 4 | 1 | 5 | 6 | 9 |
第二趟排序(外循环)
第一次两两比较2 < 4不交换
交换前状态| 2 | 4 | 1 | 5 | 6 | 9 |
交换后状态| 2 | 4 | 1 | 5 | 6 | 9 |
第二次两两比较,4 > 1交换
交换前状态| 2 | 4 | 1 | 5 | 6 | 9 |
交换后状态| 2 | 1 | 4 | 5 | 6 | 9 |
第三次两两比较,4 < 5不交换
交换前状态| 2 | 1 | 4 | 5 | 6 | 9 |
交换后状态| 2 | 1 | 4 | 5 | 6 | 9 |
第四次两两比较,5 < 6不交换
交换前状态| 2 | 1 | 4 | 5 | 6 | 9 |
交换后状态| 2 | 1 | 4 | 5 | 6 | 9 |
第三趟排序(外循环)
第一次两两比较2 > 1交换
交换后状态| 2 | 1 | 4 | 5 | 6 | 9 |
交换后状态| 1 | 2 | 4 | 5 | 6 | 9 |
第二次两两比较,2 < 4不交换
交换后状态| 1 | 2 | 4 | 5 | 6 | 9 |
交换后状态| 1 | 2 | 4 | 5 | 6 | 9 |
第三次两两比较,4 < 5不交换
交换后状态| 1 | 2 | 4 | 5 | 6 | 9 |
交换后状态| 1 | 2 | 4 | 5 | 6 | 9 |
第四趟排序(外循环)无交换
第五趟排序(外循环)无交换
大家原理应该看懂了吧!那接下来就上代码,这里的全贴出来,只是说一下代码当中哪些要注意的地方。
//冒泡排序
void Bubblesort(int A[],int len)
{
<span style="white-space:pre"> </span>bool flag= false;
<span style="white-space:pre"> </span>for(int i=len-1;i>=0;i--)
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>for(int j=0;j<i;j++)
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span> if(A[j]>A[i])
<span style="white-space:pre"> </span>{
<span style="white-space:pre"> </span>int temp=A[i];
<span style="white-space:pre"> </span>A[i]=A[j];
<span style="white-space:pre"> </span>A[j]=temp;
<span style="white-space:pre"> </span>flag=true;
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>if (!flag)
<span style="white-space:pre"> </span>break;
<span style="white-space:pre"> </span>}
}
值得注意的是flag这个变量能够防止一些升序数组的无用排序。都是一些小tricks,但是小处见功底啊!
2.插入排序
插入排序是排序中比较常见的一种,理解起来非常简单。现在比如有以下数据需要进行排序:
10 3 8 0 6 9 2
当使用插入排序进行升序排序时,排序的步骤是这样的:
10 3 8 0 6 9 2 // 取元素3,去和10进行对比
3 10 8 0 6 9 2 // 由于10比3大,将10向后移动,将3放置在原来10的位置;再取8与前一个元素10进行对比
3 8 10 0 6 9 2 // 同理移动10;然后8再和3比,8大于3,所以不再移动;如此重复下去
……
0 2 3 6 8 9 10
也就是说,我们每一次取一个元素,都要将该元素与之前已经排序好的元素进行比较。
应该也能理解了吧!
下面上代码:
void insertionSort(int A[],int left,int right)
{
int temp;
int j ;
for(int i =left+1;i<=right;i++)
{
temp = A[i];
for( j = i;j>left&&A[j-1]>temp;j--)
{
A[j] = A[j-1];
}
A[j] = temp; //这个不放再for循环里面可以减少一些移动
}
代码优化也是一学问 ,比如
A[j] = temp;是否放在for循环里面。注意
A[j] = A[j-1];这句话的含义。
如果熟悉排序的朋友都知道,上面的算法的平均时间复杂度都在O(n^2);下面的算法时间复杂度就要在O(nlogn)
3.归并排序
这是一个递归算法,这个算法的理解其实可以借助下面这个图:
盗图来自 作者,感谢
或者是下面这个实例
void Merge(int A[],int left,int right)
{
int center= (left+right)/2; //数组一般大小
int Lpos=left;
int Rpos=center+1;
int Num = right-left+1;
int * B = new int [Num];
int totalPos=0;
while(Lpos<=center&&Rpos<=right)
{
if(A[Lpos]>A[Rpos])
{
B[totalPos++]=A[Rpos++];
}
else
{
B[totalPos++]=A[Lpos++];
}
}
/********剩下没有完的并在一起*****/
while(Lpos<=center)
{
B[totalPos++]=A[Lpos++];
}
while(Rpos<=right)
{
B[totalPos++]=A[Rpos++];
}
for(int i=0;i<Num;i++)
{
A[i+left]=B[i];
}
delete B;
}
void Msort(int A[],int left,int right)
{
int center;
if(left<right)
{
center=(left+right)/2;
sort(A,left,center);
Msort(A,center+1,right);
Merge(A,left,right);
}
}
代码的解释吗?感觉没什么好讲的,不懂的可以留言!
void swap(int &a,int &b)
{
int temp;
temp = a;
a = b;
b = temp;
}
int Median(int A [],int left,int right)
{
//int len = sizeof(A)/sizeof(int);
//int median;
int center = (right+left)/2;
if(A[left]>A[center])
swap(A[left],A[center]);
if(A[left]>A[right])
swap(A[left],A[center]);
if(A[center]>A[right])
swap(A[right],A[center]);
swap(A[center],A[right-1]); //经过三数中值分割法后right比枢纽元要大,所以要将枢纽元放在right-1的位置
return A[right-1];
}
void quickSort(int A[],int left,int right)
{
if(left+cutoff<=right)
{
int key = Median(A,left,right);
int j = right-2; //在这个地方犯了一个错误,在数据结构与算法分析上也有说这个错误
int i = left+1; //i从left+ 1开始,是三数中值分割法后,left比枢纽元要小
while(1)
{
while(A[i++]<key){};
while(A[j--]>key){};
if(i<j)
swap(A[--i],A[++j]); //这里做了自加和自减操作,是因为这里前面的i++和j--,所以要将索引移回去再交换,虽然是一个细节,但是当时在这里检查了好久才发现这个大虫
else
break;
}
swap(A[i],A[right-1]);
quickSort(A,left,i-1);
quickSort(A,i+1,right);
}
else
insertionSort(A,left,right);
}
关于这个算法讲额外的,就是三数中值分割法,具体原理《数据结构预算法分析c语言版》,用来选取枢纽元。
堆的定义:
n个关键字序列Kl,K2,…,Kn称为堆,当且仅当该序列满足如下性质(简称为堆性质):
(1)ki<=k(2i+1)且ki<=k(2i+2)(1≤i≤ n),当然,这是小根堆,大根堆则换成>=号。 //ki相当于二叉树的非叶结点,K2i则是左孩子,k2i+1是右孩子
若将此序列所存储的向量R[1..n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:
树中任一非叶结点的关键字均不大于(或不小于)其左右孩子(若存在)结点的关键字。
堆排序:
堆排序是一种选择排序。是不稳定的排序方法。时间复杂度为O(nlogn)。
堆排序的特点是:在排序过程中,将排序数组看成是一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲节点和孩子节点之间的内在关系,在当前无序区中选择关键字最大(或最小) 的记录。
堆排序基本思想:
1.将要排序的数组创建为一个大根堆。大根堆的堆顶元素就是这个堆中最大的元素。
2.将大根堆的堆顶元素和无序区最后一个元素交换,并将无序区最后一个位置例入有序区,然后将新的无序区调整为大根堆。重复操作,无序区在递减,有序区在递增。
完全二叉树的基本性质:
数组中有n个元素,i是节点,1 <= i <= n/2 就是说数组的后一半元素都是叶子节点。
i的父节点位置:i/2
i左子节点位置:i*2
i右子节点位置:i*2 + 1
void maxHeap(int A[],int index,int len)
{
int left = 2*index+1;
int right = 2*index+2;
int lrg= index;
if(left<len&&A[left]>A[lrg])
lrg = left;
if(right<len&&A[right]>A[lrg])
lrg = right;
if(lrg!=index)
{
swap(A[lrg],A[index]);
maxHeap(A,lrg,len/2);
}
}
void heapSort(int A[],int len)
{
//int len = sizeof(A)/sizeof(int);
for(int i=len/2-1;i>=0;i--)
maxHeap(A,i,len);
for(int i = len-1;i>0;i--)
{
swap(A[i],A[0]);
maxHeap(A,0,i);
}
}
话说,很久以前,上面的主函数是这样定义
int main()
{
int A[100000] = {0};
clock_t start,finish;
double totaltime;
srand((unsigned)time(NULL));
int index;
for(int i=0;i<100000;i++)
{
A[i]=rand();
}
cout<<"please choose number of the sorting way"<<endl;
cout<<"1.Bubble sorting "<<endl;
cout<<"2.Merge sorting"<<endl;
cout<<"3 Quick sorting"<<endl;
cout<<"4 Insertion sorting"<<endl;
cout<<"5 heap sorting"<<endl;
cin>>index;
start=clock();
switch (index)
{
case 1:
Bubblesort(A,sizeof(A)/sizeof(int));break;
case 2:
Msort(A,0,sizeof(A)/sizeof(int)-1);break;
case 3:
quickSort(A,0,sizeof(A)/sizeof(int)-1);break;
case 4:
insertionSort(A,0,sizeof(A)/sizeof(int)-1);break;
case 5:
heapSort(A,sizeof(A)/sizeof(int));break;
default:
cout<<"please input the right number"<<endl;
sleep(5);
return 0;
}
finish=clock();
totaltime=(double)(finish-start)/CLOCKS_PER_SEC;
cout<<"after sorting"<<endl;
for(int i=0;i<sizeof(A)/sizeof(int);i++)
{
cout<<A[i]<<" ";
}
cout<<endl;
cout<<"and time consume is "<<totaltime<<endl;
}
时间大概39s
下面是归并排序
0.04秒
这是快速排序,书上说快速排序比归并快一些,果真!
0.03秒
插入排序
16秒多,比冒泡排序快了1倍多。
然后最后一个堆排序最让我意外。比较堆排序还有归并排序,两者都是用了递归,平均算法时间复杂度相同。仔细分析,也是如此。
上面造成的一些差异,无论正常还是不正常(其实不正常),说正常是因为实际结果确实是这样。说不正常也是有原因得,和理想的情况不一样。例如冒泡排序还有插入排序,结果还是挺大,当然实际还有考虑内存等其他计算资源。到底为什么这样。欢迎大家一起讨论。折算时留了一个疑问吧
记下来以观后想,或者大家讨论
2.排序算法时间复杂度分析及分治法
这一部分我也没有什么很深的了解,写这篇文章的时候也是第一次系统的了解,这篇博文讲了分治法。
应该说,分治法在实际当中常常是用在一起的的,典型的应用就像是归并排序。
这里列出分治法用在的一些地方。常常解决的十个问题。有机会自己会尝试着看一看。
可使用分治法求解的一些经典问题
(1)二分搜索
(2)大整数乘法
(3)Strassen矩阵乘法
(4)棋盘覆盖
(5)合并排序
(6)快速排序
(7)线性时间选择
(8)最接近点对问题
(9)循环赛日程表
(10)汉诺塔github上的代码还没有上传上去,马上就快上传去了