数据结构之九大排序

本帖转载自:https://blog.csdn.net/gyhgx/article/details/60468952

最近面临实习面试,由于自己准备投开发岗,据了解在面试中对于数据结构的考察是很重要的,其中对于查找、排序的算法考察尤为重要,所以又重回当年学习的数据结构好好复习。

参考:《数据结构》 严蔚敏版、《考研数据结构》及各排序总结博客,整理出适合自己的排序总结以供参考

http://www.open-open.com/lib/view/open1453126714683.html
http://www.cnblogs.com/leeplogs/p/5863846.html
http://blog.csdn.net/cold702/article/details/7979332
http://blog.csdn.net/zgrjkflmkyc/article/details/11639091
http://blog.csdn.net/whuslei/article/details/6442755


排序基本概念


分类

  1. 插入排序:直接插入排序、二分法插入排序、希尔排序。
  2. 选择排序:简单选择排序、堆排序。
  3. 交换排序:冒泡排序、快速排序。
  4. 归并排序
  5. 基数排序

排序图
其中快排的空间复杂度应该是log2 n


稳定性

排序前:if Ai==Aj && i<j
排序后:Ai仍在Aj之前
 //稳定:冒泡排序、直接插入排序、二分法插入排序,归并排序和基数排序
 //不稳定:简单选择排序、快速排序、希尔排序、堆排序

从稳定性来看,所有时间复杂度为O(n^2)的简单排序和基数排序都是稳定的。而快排、堆排、希尔排序等时间性能较好的排序都是不稳定的。(一般来说,排序过程中的“比较”是在“相邻两个记录关键字”间进行的排序都是稳定的)
(有个猜测,方便记忆:一般来说,若存在不相邻元素间交换,则很可能是不稳定的排序。)


性能

O(n^2):直接插入排序,简单选择排序,冒泡排序。

在数据规模较小时(9W内),直接插入排序,简单选择排序差不多。当数据较大时,冒泡排序算法的时间代价最高。性能为O(n^2)的算法基本上是相邻元素进行比较,基本上都是稳定的。

O(nlogn):快速排序,归并排序,希尔排序,堆排序。

其中,快排是最好的, 其次是归并和希尔,堆排序在数据量很大时效果明显。

排序算法选择

1.数据规模较小

(1)待排序列基本有序的情况下,可以选择直接插入排序;

(2)对稳定性不作要求宜用简单选择排序,对稳定性有要求宜用插入或冒泡

2.数据规模不是很大

(1)完全可以用内存空间,序列杂乱无序,对稳定性没有要求,快速排序,此时要付出log(N)的额外空间。

(2)序列本身可能有序,对稳定性有要求,空间允许下,宜用归并排序

3.数据规模很大

(1)对稳定性有求,则可考虑归并排序。
(2)对稳定性没要求,宜用堆排序。

4.序列初始基本有序(正序),宜用直接插入,冒泡


各排序详细介绍


插入排序

####1、直接插入排序####
针对一个已排好序的序列,将待排数据插入到该序列中的合适位置,插入完成后序列依然有序。

排序算法
(1)从第一个元素开始,该元素可以认为已经被排序,
(2)取出下一个元素,在已经排序的元素序列中从后向前扫描,
(3)如果该元素(已排序)大于新元素,将该元素移到下一位置,
(4)重复步骤3,直到找到已排序的元素小于或者等于新元素的位置,
(5)将新元素插入到下一位置中,
(6)重复步骤2;

稳定性
在插入排序中,K1是已排序部分中的元素,当K2和K1比较时,直接插到K1的后面(没有必要插到K1的前面,这样做还需要移动!!),因此,插入排序是稳定的。

复杂度
一般情况下,插入排序的时间复杂度和空间复杂度分别为 O(n2 ) 和 O(1) 。

JAVA实现:

 /**
 * Created by hgx on 2017/3/5.
 * 直接插入排序
 */
public static int[] insertSort(int[] a){
    //从数组第二个元素开始排序
    for(int i=1;i&lt;a.length;i++){
        int temp = a[i];//缓存待排数据
        int j = i-1;   //从右向左在有序区a[0...i-1]中找a[i]的插入位置
        //将大于temp的数据后移
        while (j&gt;=0 &amp;&amp; temp&lt;a[j]){
            a[j+1] = a[j--];
            }
        //在j+1处插入待排数据
        a[j+1] = temp;     
    }
    return a;
}

这里写图片描述

####2、二分插入排序####
二分排序的思想和直接插入排序是一样的,只是寻找插入位置的方式不同。按照二分法寻找位置,大大减少比较次数。二分插入就是首先将队列中取最小位置low和最大位置high,然后算出中间位置mid,将中间位置mid与待插入的数据data进行比较,如果mid大于data,则就表示插入的数据在mid的左边,high=mid-1,如果mid小于data,则就表示插入的数据在mid的右边,low=mid+1。

排序算法
(1)确定查找范围low=0,high=a.length-1,计算中项mid=(low+high)/2。
(2)若a [mid]=temp或low>=high,则结束查找;否则,向下继续。
(3)若a[mid] < temp,说明待查找的元素值只可能在比中项元素大的范围内,则把mid+1的值赋给low,并重新计算mid,转去执行步骤2;若a [mid]>temp,说明待查找的元素值只可能在比中项元素小的范围内,则把mid-1的值赋给high,并重新计算mid,转去执行步骤2。
(4)找到新元素的插入位置后,将其按直接插入排序方法插入即可。

稳定性
二分排序是稳定的排序。

复杂度
从时间上比较,这般插入排序仅仅减少了关键字的比较次数,却没有减少记录的移动次数,故时间复杂度仍为O(n^2)。

这里写图片描述

JAVA实现:

/**
 * Created by hgx on 2017/3/5.
 * 二分排序
 */

public class BinInsertSort {
public static int[] binInsertSort(int[] a){
int low,mid,high;
int temp;
//从数组第二个元素开始排序
for (int i=1; i<a.length; i++){
temp = a[i];
low = 0;
high = i-1;
while (low<=high){
mid = (low + high) / 2;
if (temp < a[mid])
high = mid-1 ;
else
low = mid+1;
}
//将a[low]–a[i-1]的数都想后移一位
for (int j = i; j>low; j–){
a[j] = a[j-1];
}
a[low] = temp; //最后将a[i]插入a[low]
}
return a;
}

####3、希尔排序####

将需要排序的序列划分成为若干个较小的子序列,对子序列进行插入排序,通过则插入排序能够使得原来序列成为基本有序。这样通过对较小的序列进行插入排序,然后对基本有序的数列进行插入排序,能够提高插入排序算法的效率。

在希尔排序中首先解决的是子序列的选择问题。对于子序列的构成不是简单的分段,而是采取相隔某个增量的数据组成一个序列。一般的选择原则是:取上一个增量的一半作为此次序列的划分增量。首次选择序列长度的一半为增量。

排序算法
(1)取d=a.length/2作为第一个增量,把待排数组分为d1个组
(2)从第d个元素开始比较,在各组内进行直接插入排序。
(3)取d=d/2重复步骤(1)直至d=1

稳定性
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。(有个猜测,方便记忆:一般来说,若存在不相邻元素间交换,则很可能是不稳定的排序。)

复杂度
Shell排序算法的时间复杂度分析比较复杂,实际所需的时间取决于各次排序时增量的个数和增量的取值。研究证明,若增量的取值比较合理,希尔排序的平均时间复杂度为O(nlogn)

这里写图片描述

JAVA实现:

/**
 * Created by hgx on 2017/3/5.
 * 希尔排序
 */

public class ShellSort {
public static int[] shellSort(int[] a){
int d = a.length/2;
int temp;
while (d>0){
/*
* 以下这行代码需要详细解释。这个算法能够避免麻烦的分组下标表示
* i从0+d开始,因为分组后的第一组是0,d,2d,3d…这样一个序列.所以直接从d开始比较
* 此循环比较算法区别:假设有一个16项数组,下标为0~15,当d=5时,分为5组
* 以下标表示:(0,5,10,15),(1,6,11),(2,7,12),(3,8,13),(4,9,14)
* 如果分别针对每一组插入排序,则下标控制很复杂,所以外层for循环每次对每一分组的前2个数据排序
* 然后前3个,然后前4个…这和组数有关
* 即当i=5时,对(0,5,10,15)中的(0,5)排序
* i=6时,对(1,6,11)中的(1,6)排序…
* i=10时,对(0,5,10,15)中的(0,5,10)排序…
* 一直到d=1时,此时的数组基本有序,数据项移动次数大大减少
* */
for (int i=d;i<a.length;i++) {
temp = a[i];
int j = i; //从该组最右往左开始比较
//j要大于等于第一个数的下标,且temp比上一位数值要小
while (j >= d && temp < a[j-d]) {
a[j] = a[j - d]; //把上一位向后移
j -= d;
}
a[j] = temp;
}
d=d/2;
}
return a;
}


选择排序

####1、简单选择排序####
每一趟从待排序的记录中选出关键字最小的记录,顺序放在已排好序的子文件的最后,直到全部记录排序完毕。如对于一组关键字{K1,K2,…,Kn},首先从K1,K2,…,Kn中选择最小值,假如它是 Kz,则将Kz与 K1对换;然后从K2,K3,…,Kn中选择最小值 Kz,再将Kz与K2对换。如此进行选择和调换n-2趟,第(n-1)趟,从Kn-1、Kn中选择最小值 Kz将Kz与Kn-1对换,最后剩下的就是该序列中的最大值,一个由小到大的有序序列就这样形成。

稳定性
一般认为,若是从前往后比较来选择第i小的记录则是稳定的,若是从后往前比较则不稳定。

复杂度
选择排序法与冒泡排序法一样,最外层循环仍然要执行n-1次,其效率仍然较差。该算法的时间复杂度为 O(n2)。

这里写图片描述

JAVA实现:

/**
 * Created by hgx on 2017/3/6.
 * 简单选择排序
 */

public class SelectSort {
public static int[] selectSort(int[] a){
int temp;
int k,min; //最小数下标K,值为min
for (int i =0;i<a.length-1;i++) { //第i趟排序
k = i;
min = a[i];
//从i+1位开始检索最小值
for (int j = i + 1; j < a.length; j++) {
if (a[j] < min) { //找到最小值并更新min
min = a[j];
k = j;
}
}
if (k != i) { //将找到的最小值与第i位交换
a[k] = a[i];
a[i] = min;
}
}
return a;
}

####2、堆排序####

堆的概念: 一棵完全二叉树,任一个非终端结点的值均小于等于(或大于等于)其左、右儿子结点的值。堆分为大顶堆和小顶堆,满足Key[i]>=Key[2i+1]&&key>=key[2i+2]称为大顶堆,满足Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]称为小顶堆。由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。
例:
这里写图片描述

堆排序利用完全二叉树中双亲节点和孩子节点之间的内在关系,在当前无序区中选择关键字最大(或者最小)的记录。也就是说,以最小堆为例,根节点为最小元素,较大的节点偏向于分布在堆底附近。
然后,再对剩下的n-1个元素建成堆,得到n个元素中关键码次大(或次小)的元素。以此类推,直到进行N-1此后,排序结束,便得到一个按关键码有序的序列。

初始序列:30,24,85,16,36,53,91,47
首先,将待排序序列建成大顶堆:
这里写图片描述
利用此大顶堆进行堆排序:
这里写图片描述

稳定性
堆排序为不稳定排序,不适合记录较少的排序。

复杂度
从上述过程可知,堆排序其实也是一种选择排序,是一种树形选择排序。只不过直接选择排序中,为了从R[1…n]中选择最大记录,需比较n-1次,然后从R[1…n-2]中选择最大记录需比较n-2次。事实上这n-2次比较中有很多已经在前面的n-1次比较中已经做过,而树形选择排序恰好利用树形的特点保存了部分前面的比较结果,因此可以减少比较次数。对于n个关键字序列,最坏情况下每个节点需比较log2(n)次,因此其最坏情况下时间复杂度为nlogn。

JAVA实现:

/**
 * Created by hgx on 2017/3/6.
 * 堆排序
 */

public class HeapSort {
public static int[] heapSort(int[] a){
int arrayLength = a.length;
//循环建堆
for (int i =0; i<arrayLength-1;i++){
//建堆
buildMaxHeap(a,arrayLength-1-i);
//交换堆顶和最后一个元素
swap(a,0,arrayLength-1-i);
}
return a;
}
public static void buildMaxHeap(int[] data,int lastIndex){
//从lastIndex处节点(最后一个节点)的父节点开始
for (int i = (lastIndex-1)/2;i>=0;i–){
int k = i;//k保存正在判断的节点
//如果当前K节点的子节点存在
while (k2+1<=lastIndex){
int biggerIndex = 2
k +1;//k节点的左子节点的索引
//如果biggerIndex小于lastIndex,即biggerIndex+1代表k节点的右子节点存在
if (biggerIndex<lastIndex){
//如果右子节点的值较大
if (data[biggerIndex]<data[biggerIndex+1]){
biggerIndex++; //biggerIndex总是记录较大子节点的索引
}
}
//如果k节点的值小于其较大的子节点的值
if (data[k]<data[biggerIndex]){
//交换他们
swap(data,k,biggerIndex);
//将biggerIndex赋予k,开始while循环的下一次循环,重新保证k节点的值大于其左右子节点的值
k=biggerIndex;
}else {
break;
}
}
}
}

private static void swap(int[] data,int i ,int j){
    int temp = data[i];
    data[i] = data[j];
    data[j]=temp;
}

交换排序

####1、冒泡排序####

冒泡排序(Bubble Sort)是一种最直观的排序方法,在排序过程中,将相邻的记录的关键字进行比较,若前面记录的关键字大于后面记录的关键字,则将它们交换,否则不交换。或者反过来,使较大关键字的记录后移,像水中的气泡一样,较小的记录向前冒出,较大的记录像石头沉入后部。故称此方法为冒泡排序法。
这里写图片描述

稳定性
序过程中只交换相邻两个元素的位置。因此,当两个数相等时,是没必要交换两个数的位置的。所以,它们的相对位置并没有改变,冒泡排序算法是稳定的!

复杂度
冒泡排序算法稳定,O(1)的额外的空间,比较和交换的时间复杂度都是O(n^2),自适应,对于已基本排序的算法,时间复杂度为O(n)。冒泡算法的许多性质和插入算法相似,但对于系统开销高一点点。使用冒泡排序法对n个数据进行排序,共需要进行n-1次的比较。如果本来就是有顺序的数据,也需要进行n-1次比较。冒泡排序法的算法很简单,效率也较差。

JAVA实现:

/**
 * Created by hgx on 2017/3/6.
 * 冒泡排序
 */

public class BubbleSort {
public static int[] bubbleSort(int[] a){
int temp;
for (int i=0;i<a.length-1;i++){
for (int j =0;j<a.length-i-1;j++){
if (a[j]>a[j+1]){
temp = a[j];
a[j]=a[i];
a[i]=temp;
}
}
}
return a;
}

####2、快速排序####
快速排序(Quick Sorting)是对冒泡排序的一种改进。在冒泡排序中,记录的比较和交换是在相邻的单元中进行的,记录每次交换只能上移或下移一个单元,因而总的比较和移动次数较多。而在快速排序中,记录的比较和交换是从两端向中间进行的,关键字较小的记录一次就能从后面单元交换到前面去,而关键字较大的记录一次就能从前面的单元交换到后面的单元,记录每次移动的记录较远,因此可以减少记录总的比较和移动次数。

选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地排序划分的两部分。

这里写图片描述

排序方法
设待排序序列为r[s…t],为实现一次划分,可设置两个指针low和high,他们的初值分别为s和t。以r[s]为基准,在划分的过程中:

(1)从high端开始,依次向前扫描,并将扫描到的每一个记录的关键字同r[s](即基准记录)的关键字进行比较,直到r[high].key< r[s].key时,将r[high]赋值到low所指的位置。

(2)从low端开始,依次向后扫描,并将扫描到的每一个记录的关键字同r[s](即基准记录)的关键字进行比较,直到r[low].key> r[s].key时,将r[low]赋值到high所指的位置。

(3)如此交替改变扫描方向,重复上述两个步骤从两端各自向中间位置靠拢,直到low等于或大于high。经过此次划分后得到的左右两个子序列分别为r[s…low-1]和r[low+1…t]。然后对这两个子序列按上述方法进行再次划分,依次重复,直到每个序列只剩一个元素为止。

稳定性
快速排序是一个不稳定的排序方法。

复杂度
若快速排序出现最好的情况(左、右子序列的长度大致相等),则结点数n与二叉树深度h应满足log2(n)<=h<=log2(n+1),所以总的比较次数不会超过(n+1)log2(n).因此,快速排序的最好时间复杂度应为O(nlog2(n))。若快速排序出现最坏的情况(每次能划分成两个子序列,但是其中一个为空),则此时得到的二叉树是一棵单枝树,得到的非空子序列包含有n-i(i代表二叉树的层数),每层划分需要比较n-i+2次,所以总的比较次数为(n2+3n-4)/2.因此,快速排序的最坏时间复杂度为O(n2).
另外,由于快速排序是递归的,每层递归调用时的指针和参数均要用栈来存放,存放开销在理想情况下为O(log2n),即树的高度。在最坏情况下为O(n)。

JAVA实现:

/**
 * Created by hgx on 2017/3/6.
 * 快速排序
 */

public class QuickSort {
public static int[] quickSort(int[] a, int low, int high){
if (low < high){
int middle = getMiddle(a,low,high);
quickSort(a,0,middle-1);
quickSort(a,middle+1,high);
}
return a;
}

public static int getMiddle(int[] a,int low, int high){
    int temp = a[low];
    while (low&lt;high){
        //从右向左找比基准小的元素并交换
        while (low&lt;high &amp;&amp; a[high]&gt;=temp){
            high--;
        }
        a[low]  = a[high];
        //从左往右找比基准大的元素并交换
        while (low&lt;high &amp;&amp; a[low]&lt;=temp){
            low ++;
        }
        a[high] = a[low];
    }
    a[low] =temp;
    return low;
}

归并排序

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

这里写图片描述

排序方法
1、递归基础:若序列只有一个元素,则它是有序的,不执行任何操作

2、递归步骤:
先把序列划分成长度基本相等的两个序列
对每个子序列递归排序
把排好序的子序列归并成最后的结果

3、合并步骤:
(1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置
(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
(4)重复步骤3直到某一指针达到序列尾
(5)将另一序列剩下的所有元素直接复制到合并序列尾

稳定性
归并排序最大的特色就是它是一种稳定的排序算法。归并过程中是不会改变元素的相对位置的。

复杂度
一趟归并需要n次,总共需要logN次,因此为O(N*logN).
数组需要O(n)的额外空间

JAVA实现:

/**
 * Created by hgx on 2017/3/6.
 * 归并排序
 */

public class MergeSort {
public static int[] mergeSort(int[] a,int left,int right){
if (left < right){
//划分为两部分,每次两部分进行归并
int middle = (left + right)/2;
//两路归并,先递归处理每一部分
mergeSort(a,left,middle);
mergeSort(a,middle+1,right);
//将已排好序的两两归并排序进行合并
merge(a,left,middle,right);
}
return a;
}
private static void merge(int[] a, int left, int middle, int right){
int[] tempArr = new int[a.length];//临时数组tempArr[],用来存放待合并的两路归并排序数组
int rightIndex = middle +1;//右数组第一个元素索引
int tempIndex = left;//记录临时数组的索引
int tmp = left;//缓存左数组第一个元素位置
while (left <= middle && rightIndex<=right){
//从两个数组中取出最小的放入临时数组
if (a[left]<=a[rightIndex]){
tempArr[tempIndex++] = a[left++];
}else {
tempArr[tempIndex++] = a[rightIndex++];
}
}
//剩余部分以此放入临时数组。以下两个while只会执行其中一个
while (left<=middle){
tempArr[tempIndex++] = a[left++];
}
while (rightIndex<=right){
tempArr[tempIndex++] = a[rightIndex++];
}
//将临时数组中的内容拷贝回原数组
while (tmp<=right){
a[tmp]=tempArr[tmp++];
}
}


基数排序(BaseSort)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值