常用排序算法之一

    好像可以保研,但是最近没事也找了几份工作。看到各家的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.归并排序

这是一个递归算法,这个算法的理解其实可以借助下面这个图:

盗图来自 作者,感谢

或者是下面这个实例

并排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
例如,有数列{6,202,100,301,38,8,1}
1.  刚开始的分组如下:
  i=1 [6 202 ] [ 100 301] [ 8 38] [ 1 ] 比较次数为3次
 2. 第二次,两两分组合并
  i=2 [ 6 100 202 301 ] [ 1 8 38 ] 比较次数为4
3.  第三次,继续合并
  i=3 [ 1 6 8 38 100 202 301 ] 比较次数为4
应该有了一个大概的了解吧
来人,上代码!!

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);
	
 }
}
代码的解释吗?感觉没什么好讲的,不懂的可以留言!

4.快速排序

原理烦请参见这篇 博文
代码上面大神的里面也有,还是贴贴我自己的吧!
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语言版》,用来选取枢纽元。
三数种植分割法就是找打数组最左端,最右端还有中间的数字的中值作为枢纽元。

5.堆排序
我理解的和代码几乎都是搬这位 大神的,感谢

堆的定义:     

      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;
  
}


二.分治思想还有时间复杂度
首先贴几张图,缓解一下文字密集感。数组统一大小100000
这是冒泡排序


时间大概39s

下面是归并排序


0.04秒

这是快速排序,书上说快速排序比归并快一些,果真!


0.03秒

插入排序


16秒多,比冒泡排序快了1倍多。




然后最后一个堆排序最让我意外。比较堆排序还有归并排序,两者都是用了递归,平均算法时间复杂度相同。仔细分析,也是如此。

上面造成的一些差异,无论正常还是不正常(其实不正常),说正常是因为实际结果确实是这样。说不正常也是有原因得,和理想的情况不一样。例如冒泡排序还有插入排序,结果还是挺大,当然实际还有考虑内存等其他计算资源。到底为什么这样。欢迎大家一起讨论。折算时留了一个疑问吧

记下来以观后想,或者大家讨论

2.排序算法时间复杂度分析及分治法

这一部分我也没有什么很深的了解,写这篇文章的时候也是第一次系统的了解,这篇博文讲了分治法。

应该说,分治法在实际当中常常是用在一起的的,典型的应用就像是归并排序。

这里列出分治法用在的一些地方。常常解决的十个问题。有机会自己会尝试着看一看。

可使用分治法求解的一些经典问题

(1)二分搜索

(2)大整数乘法

(3)Strassen矩阵乘法

(4)棋盘覆盖

(5)合并排序

(6)快速排序

(7)线性时间选择

(8)最接近点对问题

(9)循环赛日程表

(10)汉诺塔

github上的代码还没有上传上去,马上就快上传去了


  


   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值