数据结构与算法:冒泡/简单选择/直接插入/堆/归并/快速排序介绍以及代码实现

7类常见排序算法介绍及代码实现

数据结构与算法:冒泡/简单选择/直接插入/堆/归并/快速排序介绍以及代码实现_第1张图片

概述:

​ 本文算法实现主要是基于java 语言。介绍了冒泡排序、简单选择排序、直接插入排序、希尔排序、堆排序、归并排序、快速排序等主要排序算法。并在文章的最后对这几类算法做了一个简单的对比。

首先了解几个概念:

排序的稳定性:

​ 对于值相等的两个元素,Ri=Rj,假设排序前Ri位于Rj之前,即i

内排序与外排序:

​ 内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。

算法分类:

按算法复杂度分类:

​ (1)冒泡排序/简单选择排序/直接插入排序均为简单算法

​ (2)希尔排序/堆排序/归并排序/快速排序均为改进算法

根据排序过程中借助的主要操作分类:

​ (1)插入排序

​ (2)交换排序

​ (3)选择排序

​ (4)归并排序

​ 如下图所示:
数据结构与算法:冒泡/简单选择/直接插入/堆/归并/快速排序介绍以及代码实现_第2张图片

1.冒泡排序:

​ 冒泡排序(Bubble Sort)一种交换排序,它的基本思想是:两两比较相邻记录的关键字,如果反序则交换,直到没有反序的记录为止。非严格意义上的冒泡排序算法可能比较的不是相邻元素。

​ 冒泡排序通过两两比较相邻记录的关键字,往往会在一次遍历中将最小值或最大值移动到数组的一端,在后续的遍历中再对其他元素的排序。

下方同时介绍了exch()方法和less()方法,后面很多排序算法中都会用到这两个方法。

Comparable[]指的是可以接受任何实现了Comparable接口的数据类,如:数组String、Arrays···

public class Sort_ {
    //交换数组的俩数据
    public static void exch(Comparable[] nums, int i, int j){
        int tempNum=nums[i];
        nums[i]=nums[j];
        nums[j]=tempNum;
    }
    //比较大小
    /*当v=w时,返回false*/
    public static boolean less(Comparable v, Comparable w){
        return v.compareTo(w)<0;
    }

排序算法:

public class Sort_ {  
	//冒泡排序(比较相邻记录)
    public static void bubble(Comparable[] nums){
        //新建一个外层循环次数的标志位
        int mark=nums.length-1;
        while(mark!=0){
            for(int i=0;i<mark;i++){
                //将更大的值往后移动
                if (nums[i]>nums[i+1]){
                    exch(nums,i,i+1);
                }
            }
            mark--;
        }
    }
    
    /*非严格意义上的冒泡排序,比较的是非相邻元素*/
	public static void bubble_(Comparable[] nums){
        for(int i=0;i<nums.length-1;i++){
            for(int j=i+1;j<nums.length;j++){
                if(less(nums[j],nums[i])){
                    exch(nums,i,j);
                }
            }
        }
    }

}

2.简单选择排序:

​ 简单选择排序法(Simple Selection Sort)就是通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1

​ 其基本思想就是:首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置。再次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。这就叫做选择排序,因为它在不断地选择剩余元素中的最小者。

特点:1.运行时间和输入无关,即使是输入有序数组,比较次数仍然和输入相同元素个数的乱序数组一样多 2.数据移动是最少的–用了N次交换,交换次数和数组大小是线性关系。
public class Sort_ {
    public static void selectSort(Comparable[] nums){
        int N=nums.length;
        for(int i=0;i<N-1;i++){
            int min=i;
            //寻找nums[i]后面元素中的最小元素
            for(int j=i+1;j<N;j++){
                if(less(nums[j],nums[min])){
                    min=j;
                }
            //将找到的最小元素的值放到nums[i]处
            exch(nums,min,i);
            }
        }
    }
}

3.直接插入排序:

​ 直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。

特点:

​ 1.和选择排序不同,插入排序所需的时间取决于输入中元素的初始顺序。

​ 2.插入排序需要的交换操作和数组中倒置的数量相同

​ 3.插入排序对于部分有序的数组十分高效,也很适合小规模数组

public class Sort_ {
    public static void insertSort(Comparable[] nums){
        int N=nums.length;
        //最初的nums[0]可认为是已序的,所以从nums[1]开始,因而下方int i=1
        for(int i=1;i<N;i++){
           	for(int j=i;j>0 && less(nums[j],nums[j-1]);j--){
                   exch(nums,j,j-1);
            }
        }
    }
}

4.希尔排序(相当于直接插入排序的升级):

特点:

​ 1.希尔排序为了加快排序速度简单的改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。

​ 2.希尔排序的实现是一个类似于插入排序但使用不同增量的过程。

​ 3.希尔排序代码量很小且不需要额外内存空间。

​ 4.希尔排序比插入排序和选择排序要快的多,并且数组越大,优势越大

​ 5.和选择排序以及插入排序相比,希尔排序也就可用于大型数组,它对任意排序的数组表现也很好。

public class Sort_ {
    public static void shellSort(Comparable[] nums){
        //将nums[]按升序排列
        int N=nums.length;
        int h=1;
        while(h<N/3) h=3*h+1; //h的值为1,4,13,40,121,364,1093,···
        while(h>=1){
            //将数组变为h有序
            /*i从h开始遍历,其思想类似于直接插入排序代码中第一次for循环的int i=1*/
            for(int i=h;i<N;i++){
                //将nums[i]插入到nums[i-h]、nums[i-2*h]、nums[i-3*h]···之中
                for(int j=i;j>=h && less(nums[j],nums[j-h]);j-=h){
                    exch(nums,j,j-h);
                }
            }
            h=h/3;
        }
    }
}

5.堆排序:

​ 堆排序(Heap Sort),就是对简单选择排序进行的一种改进,将每次排序后的结果进行记录,以便下一次排序时使用,这种改进的效果是非常明显的。堆排序算法是Floyd和Wiliams在1964年共同发明的,同时,他们发明了“堆”这样的数据结构。

堆的定义:

​ 堆是一组能够用堆有序的完全二叉树排序的元素,并且堆中的数据按照层级存储到数组中(不使用数组的第一个位置,即不适用a[0])

堆有序的定义:

​ 当一棵二叉树的每个结点都大于等于它的两个子结点时,则它被成为堆有序

​ 堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。此处我们所讲解的堆主要指大顶堆。

​ 堆排序(Heap Sort)就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。

public class Sort_ {
    //堆排序中使用的less()\exch()代码和之前算法中使用的稍有变化,但作用是一样的,特定义如下
    private boolean less(int i, int j){
        return pq[i].compareTo(pq[j]) < 0;
    }
    private void exch(int i, int j){
        Key t=pq[i];
        pq[i]=pq[j];
        pq[j]=t;
    }
    
    //堆中的下沉和上浮函数
    private void swim(int k){
        while(k>1 && less(k/2,k)){
            exch(k/2,k);
            k=k/2;
        }
    }
    private void sink(int k){
        while(2*k<=N){
            int j=2*k;
            if(j<N && less(j,j+1)) j++;
            exch(k,j);
            k=j;
        }
    } 
}

有了上面定义的类方法之后,堆排序具体实现为:

public class Sort_ {
    //堆排序代码(大顶堆)
    public static void heapSort(Comparable[] a){
        int N=a.length;
        
        //1.k所遍历到的是除了叶子结点之外的所有结点,也就是有孩子结点的结点
        for(int k=N/2;k>=1;k--){
            sink(a,k,N);
        }
        while(N>1){
            //将堆顶的最大元素与堆底的最后一个元素位置互换
            exch(a,1,N--);
            //将剩余的元素重新整理为一个堆
            sink(a,1,N);
        }
    }
}

6.归并排序:

特点:

​ 1.归并排序最吸引人的性质就是它能够保证将任意长度为N的数组排序所需时间和NlogN成正比。主要缺点就是它所需的额外空间和N成正比。

归并排序分类:
1.自顶向下的归并排序(需要先拆分递归,再归并退出递归)

​ 归并排序(Merging Sort)就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/21([x1表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。

2.自底向上的归并排序(非递归的归并排序算法)

​ 非递归的迭代做法更加直截了当,从最小的序列开始归并直至完成。不需要像归并的递归算法一样,需要先拆分递归,再归并退出递归。

7.归并方法:
public static void merge(Comparable[] a, int lo, int mid, int hi){
    //将a[lo..mid]和a[mid+1..hi]归并
    int i=lo, j=mid+1;
    
    for(int k=lo;K<=hi;k++){  //将a[lo..hi]复制到aux[lo..hi]
        aux[k]=a[k];
    }    
    
    for(int k=lo; k<=hi; k++){ //归并回到a[lo..hi]
        if(i>mid)						a[k]=aux[j++];//左半边用尽
        else if(j>hi)					a[k]=aux[i++];//右半边用尽
        else if(less(aux[j],aux[i]))	a[k]=aux[j++];
        else							a[k]=aux[i++];
    }
}
自顶向下的归并排序:

​ 该算法需要先拆分递归,再归并退出递归。该算法以递归形式实现,是分治思想的典型应用。

public class Merge{
    private static Comparable[] aux;//归并所需的辅助数组
    public static void sort(Comparable[] a){
        aux=new Comparable(a.length);
    }
    
    private static void sort(Comparable[] a, int lo, int hi){
        //将数组a[lo..hi]排序
        if(hi<=lo) return;
        int mid=lo+(hi-lo)/2;
        sort(a,lo,mid);			//将左半部分排序
        sort(a,mid+1,hi);		//将右半部分排序
        merge(a,lo,mid,hi);		//归并结果
    }
}
自底向上的归并排序:

​ 非递归方法,该方法比递归方法所需的代码量少,首先是进行两两归并,然后四四归并,之后八八归并,一直下去。

public class MergeBU{
    private static Comparable[] aux;//归并所需的辅助数组
    
    public static void sort(Comparable[] a ){
        //进行lgN次两两归并
        int N=a.length;
        aux=new Comparable[N];
        for(int sz=1; sz<N; sz=sz+sz){			//sz子数组大小
            for(int lo=0;lo<N-sz;lo+=sz+sz)		//lo:子数组索引
                merge(a,lo,lo+sz-1, Math.min(lo+sz+sz-1, N-1));
        }
    }
}

快速排序:

​ 终于我们的高手要登场了,如果将来你工作后,你的老板要让你写个排序算法,而你会的算法中竟然没有快速排序,我想你还是不要声张,偷偷去把快速排序算法找来敲进电脑,这样至少你不至于被大伙儿取笑。

​ 快速排序其实就是我们前面认为最慢的冒泡排序的升级,它们都属于交换排序类。即它也是通过不断比较和移动交换来实现排序的,只不过它的实现,增大了记录的比较和移动的距离,将关键字较大的记录从前面直接移动到后面,关键字较小的记录从后面直接移动到前面,从而减少了总的比较次数和移动交换次数。

​ 快速排序(Quick Sort)的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序的目的。

特点:

​ 1.快速排序实现简单,适用于各种不同的输入数据且在一般应用中比其他排序算法都要快得多。

​ 2.快速排序引人注目的特点包括它是原地排序(只需要一个很小的辅助栈),且将长度为N的数组排序所需的时间和NlgN成正比

​ 快速排序是一种分治的排序算法。它将一个数组分为两个数组,将两部分独立的排序。快速排序和归并排序是互补的:归并排序将数组分成了两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。归并排序中,递归调用发生在处理整个数组之前;快速排序中,递归调用发生在处理整个数组之后。

public class QuickSort{
    public static void sort(Comparable[] a, int lo, int hi){
        if(hi<=lo) return;
        int j=partition(a,lo,hi);		//切分
        sort(a,lo,j-1);					//将左半部分排序a[lo..j-1]
        sort(a,j+1,hi);					//将右半部分a[j+1..hi]排序
    }
    
    //切分方法(关键)
    private static int partition(Comparable[] a, int lo, int hi){
        //将数组切分为a[lo..i-1],a[i],a[i+1..hi]
        int i=lo, j=hi+1;			//左右扫描指针
        Comparable v=a[lo];			//切分元素
        while(true){
            //扫描左右,检查扫描是否结束并交换元素
            while(less(a[++i],v)) if(i==hi) break;
            while(less(v,a[--j])) if(i==lo) break;
            if(i>=j) break;
            exch(a,i,j);
        }
        exch(a,lo,j);		//将v=a[j]放入正确位置
        return j;			//a[lo..j-1]<=a[j]<=a[j+1..hi]达成
    }
}

切分方法的理解如下:

关于快速排序需要注意的一些地方:
1.保持随机性

​ 切分元素的获取尽量随机

2.切分元素的获取还可以采用:三数取中、九数取中
算法优化:实际上是在取枢轴数上做文章。优化方法分别有:三数取中、九数取中

算法复杂度:

数据结构与算法:冒泡/简单选择/直接插入/堆/归并/快速排序介绍以及代码实现_第3张图片

总结:

​ 1.从平均情况来看,显然最后3种改进算法要胜过希尔排序,并远远胜过前3种简单算法。
从最好情况看,反而冒泡和直接插入排序要更胜一筹,也就是说,如果你的待排序序列总是基本有序,反而不应该考虑4种复杂的改进算法。

​ 2.从最坏情况看,堆排序与归并排序又强过快速排序以及其他简单排序。

​ 3.从这三组时间复杂度的数据对比中,我们可以得出这样一个认识。堆排序和归并排序就像两个参加奥数考试的优等生,心理素质强,发挥稳定。而快速排序像是很情绪化的天才,心情好时表现极佳,碰到较糟糕环境会变得差强人意。但是他们如果都来比赛计算个位数的加减法,它们反而算不过成绩极普通的冒泡和直接插入。

​ 4.从空间复杂度来说,归并排序强调要马跑得快,就得给马吃个饱。快速排序也有相应的空间要求,反而堆排序等却都是少量索取,大量付出,对空间要求是0(1]。如果执行算法的软件所处的环境非常在乎内存使用量的多少时,选择归并排序和快速排序就不是一个较好的决策了。

​ 5.快速排序是最快的通用排序算法

​ 6.从稳定性来看,归并排序独占鳌头,我们前面也说过,对于非常在乎排序稳定性的应用中,归并排序是个好算法。

​ 7.从待排序记录的个数上来说,待排序的个数n越小,采用简单排序方法越合适。反之,n越大,采用改进排序方法越合适。

参考书籍:

​ [1] 程杰. 大话数据结构. 北京:清华大学出版社,2011

​ [2] Robert Sedgewick, Kevin Wayne. 算法(第四版). 人民邮电出版社

你可能感兴趣的:(数据结构与算法)