前言
最近在碰到排序问题时,总是会一知半解,或者想不起来某些排序算法的原理。况且排序算法在面试中也占据了一定的分量,且是很多算法的基础。因此还是结合网上已有的算法总结,来写一篇自己的排序算法总结。
排序的定义:
排序,顾名思义就是对一组数据,或者是可比较的一些对象(用java的思想,一切事物皆对象)从小到大或者从大到小进行顺序的排序(一般为从小到大)。
常见术语:
稳定:如果两个数或者对象相同,假设为a.b(即比较的属性相同时),如果未排序前a在b前面,则排序后a一定在b前面。这种排序视为稳定的。一个思考是为什么要提出排序稳定的概念,在哪里会得到应用吗?
不稳定:与稳定相对。a可能会出现在b的后面。
内排序:所有的操作都在内存中完成。
外排序:一般是由于数据太大,在内存中无法放下所有的数据,需要借助磁盘来完成排序。
时间复杂度:算法执行所耗费的时间(可以这样解释,但是不够准确)
空间复杂度:运行程序所需内存的大小。
常见排序算法比较
常见排序算法分类:
图1:常见排序算法分类(引自参考文章1)
冒泡排序(Bubble Sort)
冒泡排序,在生活中,水中的泡泡从水底到水面的过程中会由于压强的原因,泡泡逐渐变大,借鉴这个思想,一次从水底到水面的冒泡,将会使得最大的值到水面,因此下次比较就可以只比较水面下的一层即可。
具体算法描述:
<1>.先比较前两个数,如果1大于2,则进行交换,
<2>重复1的过程直到末尾,则完成一次冒泡,则最后一个数为最大值
<3>重复1 2的过程直到结束(不包括最后一个数)
代码如下:
public void sort(int[] arr){
int len = arr.length;
if(len<=1)
return ;
for(int i =len;i>0;i--){
for(int j =1;j<i;j++){
if(arr[j-1]>arr[j]){
swap(arr,j-1,j);
}
}
}
}
public void swap(int[] arr, int i, int j) {
// TODO Auto-generated method stub
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
@Test
public void sortTest(){ //测试
int[] arr = {2,3,1,4,2,0,5,-1,2};
sort(arr);
for(int i =0;i<arr.length;i++){
System.out.println(arr[i]);
}
}
算法分析:
1.是否稳定?
稳定
2.内外排序?
内排序
3.复杂度分析
最佳情况:
输入数据为正序,复杂度为T(n)= O(n)
最差情况:
输入数据为反序,复杂度为T(n)= O(n2)
平均情况:T(n)= O(n2)
在参考文章1中,存在两种简单的改进方法,有兴趣的可以参考一下。
选择排序(Selection Sort)
选择排序:
每次在未排序的序列中选择出最小的值或者最大值,将选择出来的值放在队首或者队末
具体算法描述:
<1>先比较前两个数,如果a>b.则将其下标。
<2>重复1的过程直到末尾,然后将当前最小值放到第i次的位置
<3>重复1 2的过程直到结束(不包括最后一个数)
代码如下:
public void selectSort(int[] arr){
int len = arr.length;
if(len<=1){
return ;
}
int minIndex=0;
for(int i =0;i<len-1;i++){
minIndex=i;
for(int j=i+1;j<len;j++){
if(arr[j]<arr[minIndex]){
minIndex=j;
}
}
swap(arr,i,minIndex);
}
}
@Test
public void selectSortTest(){//测试
int[] arr = {2,3,1,4,2,0,5,-1,2};
selectSort(arr);
for(int i =0;i<arr.length;i++){
System.out.println(arr[i]);
}
}
算法分析:
1.是否稳定?
稳定
2.内外排序?
内排序
3.复杂度分析
最佳情况:
最差情况:
平均情况:
全是T(n) = O(n2),
插入排序(Insertion Sort)
插入排序:
将队列分为有序队列和无序队列,从无序队列取出一个值,插入有序队列,直到无序队列中没有元素.
具体算法描述:
<1>第一个元素可以被当做是有序元素
<2>取下一个元素,在已经排序的元素队列中从后往前扫描,
<3>如果该元素大于这个取出的元素,则该元素后移一位
<4>直到当前元素等于或者小于取出的元素,则放在当前位置+1的位置
<5>重复2-3-4的过程
代码如下:
public void insertSort(int[] arr){
int len = arr.length;
if(len<=1)
return ;
for(int i =1;i<len;i++){
int temp =arr[i];
int j =i-1;
while(j>=0&&arr[j]>temp){
arr[j+1]=arr[j];
j--;
}
arr[j+1]=temp;
}
}
@Test
public void insertSortTest(){
int[] arr = {2,3,1,4,2,0,5,-1,2,8,7};
insertSort(arr);
for(int i =0;i<arr.length;i++){
System.out.println(arr[i]);
}
}
算法分析:
1.是否稳定?
稳定
2.内外排序?
内排序
3.复杂度分析
最佳情况:
正序时:T(n) = O(n)
最差情况:
反序时:T(n) = O(n2)
平均情况:
T(n) = O(n2)
改进方案,在每次查找插入点的时候,由于之前的序列是属于有序序列,因此可以通过2分插入来提高速度。
希尔排序(Shell Sort)
希尔排序:
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。
增量序列:
在这里选择增量序列gap = length/2;然后gap会继续与gap/2的方式进行递减,将{n/2,n/2/2,…,1}称为增量序列(注:增量序列的选取不限于这一种取法,如果合理的选取增量序列也是值得讨论的)。假设当前增量为5时,小标为0将与5,10,等等的序列进行插入排序。
具体算法描述:
<1>首先选择一个增量序列,
<2>根据增量序列进行k趟排序
<3>改变增量序列,重复1-2过程,直到增量等于0停止
代码如下:
public void shellSort(int[] arr){
int len = arr.length;
if(len<=1)
return ;
for(int gap = len/2;gap>0;gap/=2){
for(int i = gap;i<len;i++){
int temp = arr[i];
int j =i;
while(j>=gap&&arr[j-gap]>temp){
arr[j] = arr[j-gap];
j-=gap;;
}
arr[j] = temp;
}
}
}
@Test
public void shellSortTest(){
int[] arr = {2,3,1,4,2,0,5,-1,2,8,7};
shellSort(arr);
for(int i =0;i<arr.length;i++){
System.out.println(arr[i]);
}
}
算法分析:
1.是否稳定?
不稳定
2.内外排序?
内排序
3.复杂度分析
经过优化后的增量序列可以在最坏情况下达到O(n3/2)的复杂度。
归并排序(Merge Sort)
归并排序:
归并排序是建立在归并操作上的简单排序。采用分治的思想,归并排序是一种稳定的排序方法。
将有序的子序列进行合并,得到完全有序的序列。
具体算法描述:
<1>将长度为n的序列分成长度为n/2的子序列,直到子序列的长度为1或者0时。
<2>再将两个子序列进行归并排序。
<3>重复执行2得到最终的有序序列。
代码如下:
public void mergeSort(int[] arr){
int len = arr.length;
int[] tempArr = new int[len];
mergeSort(arr,tempArr,0,len-1);
}
private void mergeSort(int[] arr,int[] tempArr,int start,int end){
if(start<end){
int center = (start+end)/2;
mergeSort(arr,tempArr,start,center);
mergeSort(arr,tempArr,center+1,end);
merge(arr,tempArr,start,center,end);
}
}
private void merge(int[] arr,int[] tempArr,int start,int leftEnd, int end){
int num = end - start +1;
int left = start;
int right_start = leftEnd+1;
while(left<=leftEnd&&right_start<=end){
if(arr[left]<=arr[right_start]){
tempArr[start++] = arr[left++];
}else{
tempArr[start++] = arr[right_start++];
}
}
while(left<=leftEnd){
tempArr[start++] = arr[left++];
}
while(right_start<=end){
tempArr[start++] = arr[right_start++];
}
for(int i=0;i<num;i++,end--){
arr[end] = tempArr[end];
}
}
@Test
public void mergeSortTest(){//测试
int[] arr = {2,3,1,4,2,0,5,-1,2,8,7};
mergeSort(arr);
for(int i =0;i<arr.length;i++){
System.out.print(arr[i]+",");
}
}
算法分析:
1.是否稳定?
稳定
2.内外排序?
内排序
3.复杂度分析
最佳情况:
T(n) = O(n);
最差情况:
T(n) = O(nlogn);
平均情况:
T(n) = O(nlogn);
快速排序(Quick Sort)
快速排序:
快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一次排序将序列分割成两部分,其中一部分给另外一个部分的数据都要小,然后再按照这个方法,处理分割的两部分,从而使得整个序列变得有序。
具体算法描述:
<1>首先选取一个基准p,将小于p的部分放在左边,大于p的部分放在右边,(注:基准选为子数组的第一个元素较为常见)
<2>然后再对余下的两部分分别进行1中的部分,当子序列长度为1时,则直接返回
<3>最终会得到有序的序列
代码如下:
public void quickSort(int[] arr){
if(arr==null)
return;
int len = arr.length;
if(len<=1)
return ;
quickSortM(arr,0,len-1);//注意为长度减1,因为包含左包含右
}
public void quickSortM(int[] arr,int startIndex,int endIndex){//包含左包含右
if(startIndex>=endIndex)
return ;
int boundary = divide(arr,startIndex,endIndex);
quickSortM(arr,startIndex,boundary-1);
quickSortM(arr,boundary+1,endIndex);
}
public int divide(int[] arr,int startIndex,int endIndex){
int standard = arr[startIndex];
int leftIndex = startIndex;
int rightIndex= endIndex;
while(leftIndex<rightIndex){
while(leftIndex<rightIndex&&arr[rightIndex]>=standard){
rightIndex--;
}
arr[leftIndex] = arr[rightIndex];
while(leftIndex<rightIndex&&arr[leftIndex]<=standard){
leftIndex++;
}
arr[rightIndex] = arr[leftIndex];
}
arr[leftIndex] = standard;
return leftIndex;
}
@Test
public void quickSortTest(){
int[] arr = {2,3,1,4,2,0,5,-1,2,8,7};
quickSort(arr);
for(int i =0;i<arr.length;i++){
System.out.println(arr[i]);
}
}
算法分析:
1.是否稳定?
不稳定
2.内外排序?
内排序
3.复杂度分析
最佳情况:
T(n) = O(nlogn)
分析:假如每次取到的基准都是中位数,那么左右子序列的数量就是相同的,因此迭代的深度为log2n,每次的计算量就是n,因此算法时间复杂度为 O(nlogn)
最差情况:
每次取得的基准都是最小的数,或者最大的数,则每次取值使得序列长度减1,复杂度为O(n2)
平均情况为O(nlogn)
注:此部分参考文章3.
堆排序(Heap Sort)
堆排序:
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。
堆:
堆是一种完全二叉树,并且具有以下性质:左节点和右节点的值都小于父节点的叫最大堆,左右节点都大于父节点的叫最小堆。
堆排序的基本思想:
首先将待排序列构造成一个最大堆,然后使用最大堆根节点与n-i的元素(i为第i次重复),然后调整使其成为一个最大堆。重复n次上述过程。
具体算法描述:
<1>首先构造一个最大堆。
<2>最大堆的根节点和n-i的元素进行交换
<3>然后再调整使其成为一个最大堆
<4>重复2 3过程n次
接下来介绍完全二叉树的性质:
介绍完全二叉树的性质是为了编程易于理解,注:这里的序列的下标是以1而不是0开始的。
1:如果当前节点为i,则父节点为floor(i/2);
2:如果2i<=n,则存在左孩子,且左孩子为2i;
3:如果2i>n,则无左孩子;
4: 如果2i+1<=n;则存在右孩子,为2i+1;
5:如果2i+1>n,则节点i无右孩子;
注:如果使用数组来考虑的,下标是从0开始的,因此性质有所改变
1:如果当前节点为i,父节点为(i-1)/2;
2:如果2i+1< n,则存在左孩子,且左孩子为2i+1;
3:如果2i+2< n,则存在右孩子,且右孩子为2i+2;
代码如下:
public void heapSort(int[] arr){
//构建最大堆的过程
for(int i = arr.length/2-1;i>=0;i--){
adjustHeap(arr,i,arr.length);
}
for(int j=arr.length-1;j>0;j--){
swap(arr,0,j);
adjustHeap(arr,0,j);
}
}
public void adjustHeap(int[] arr,int i,int len){
int temp = arr[i];
for(int k=2*i+1;k<len;k=k*2+1){
if(k+1<len&&arr[k]<arr[k+1]){
k++;
}
if(temp<arr[k]){
swap(arr,i,k);
i=k;
}else{
break;
}
}
//arr[i] = temp;
}
@Test
public void heapSortTest(){//测试
int[] arr = {2,3,1,4,2,0,5,-1,2};
heapSort(arr);
for(int i =0;i<arr.length;i++){
System.out.println(arr[i]);
}
}
算法分析:
1.是否稳定?
不稳定
2.内外排序?
内排序
3.复杂度分析
最佳情况:
T(n) = O(nlogn)
最差情况:
T(n) = O(nlogn)
平均情况为T(n) = O(nlogn)
堆排序分为两部分,第一部分为建立最大堆的过程经推导为O(n)复杂度(值得推敲),第二部分需要交换n-1次,根据二叉树的性质每次最多log2n的复杂度,因此总的复杂度为O(nlogn)的复杂度
注:此部分参考文章4.
参考文章:
1,https://www.cnblogs.com/jztan/p/5878630.html
2,https://www.cnblogs.com/chengxiao/p/6104371.html
3,http://blog.youkuaiyun.com/lemon_tree12138/article/details/50622744
4,https://www.cnblogs.com/chengxiao/p/6129630.html