排序(一)归并、快排、优先队列等(图文具体解释)

排序(一)


0基础排序算法


选择排序


思想:首先,找到数组中最小的那个元素。其次,将它和数组的第一个元素交换位置。再次。在剩下的元素中找到最小的元素。将它与数组的第二个元素交换位置。

如此往复,直到将整个数组排序。

 

【图例】


图中。x轴方向为数组的索引,y轴方向为待排序元素的值。

 

选择排序有两个非常鲜明的特点:

执行时间和输入无关。为了找出最小的元素而扫描一遍数组并不能为下一遍扫描提供什么信息。这样的性质在某些情况下是缺点。

(不管数组的初始状态是什么,此算法效率都一样低效)

数据移动是最少的

每次交换都会改变两个数组元素的值,因此选择排序用了N次交换——交换次数和数组的大小是线性关系。(我们将研究的其它不论什么算法都不具备这个特征)

 

【对于长度为N的数组,选择排序须要大约N2/2次比較和N次交换】

 

冒泡排序


思想:它反复地走訪过要排序的数列。一次比較两个元素,假设他们的顺序错误就把他们交换过来。

走訪数列的工作是反复地进行直到没有再须要交换。也就是说该数列已经排序完毕。这个算法的名字由来是由于越小的元素会经由交换慢慢“浮”到数列的顶端。(较大的元素也会慢慢沉究竟部。

 

冒泡排序算法的运作例如以下:

1、比較相邻的元素。

假设第一个比第二个大,就交换他们两个。

2、对每一对相邻元素作相同的工作,从開始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数

3、针对全部的元素反复以上的步骤,除了最后一个。

4、持续每次对越来越少的元素反复上面的步骤,直到没有不论什么一对数字须要比較。

 

【图例】


图中,x轴方向为数组的索引。y轴方向为待排序元素的值。

由图中可看出。冒泡排序是从后到前,逐步有序的,最大的元素先沉究竟部。接着是次大的……


void BubbleSort(Comparable[] a)
{
    //exchanged表示是否做过交换处理,一趟冒泡没有做过交换处理,即没有改变不论什么元素的位置,则停止冒泡。
    bool exchanged = false ; 

    int N = a.length ;
    
    //最多n-1趟冒泡排序(若发现某趟排序没有交换操作,则停止冒泡) 
    for (int i = 1; i < N && !exchanged; i++)
    {
        exchanged = false ;
        //从第0个到第N-1-i个 为一趟冒泡,选择出此趟中最大的值,沉究竟部。
        for(j = 0; j < N-i; j++)
        {
            //若相邻的两keyword逆序 交换 
            if(a[j] > a[j+1])
            {
                exch(a[j], a[j+1]) ;    //交换a[j]、a[j+1]
                exchanged = true ;   //标记有元素交换位置 
            }
        }//for(j)
    }
}

 

特点

冒泡排序是与插入排序拥有相等的运行时间。可是两种法在须要的交换次数却非常大地不同。在最坏的情况。冒泡排序须要O(n2)次交换,而插入排序仅仅要最多O(n)交换。冒泡排序的实现(类似以下)一般会对已经排序好的数列拙劣地运行O(n2)。而插入排序在这个样例仅仅须要O(n)个运算。

因此非常多实现中避免使用冒泡排序,而用插入排序代替之

冒泡排序假设能在内部循环第一次运行时,使用一个旗标来表示有无须要交换的可能,也有可能把最好的复杂度减少到O(n)。在这个情况。在已经排序好的数列就无交换的须要。

(比如上面代码)

 

 

插入排序


思想:将第i个元素与其左边的已经有序的元素一一比較,找到合适的位置,插入当中。为了给要插入的元素腾出空间,我们须要将其余全部元素在插入之前都向右移动一位。


详细算法描写叙述例如以下:

1、从第一个元素開始,该元素能够觉得已经被排序

2、取出下一个元素。在已经排序的元素序列中从后向前扫描

3、假设该元素(已排序)大于新元素。将该元素移到下一位置

4、反复步骤3,直到找到已排序的元素小于或者等于新元素的位置

5、将新元素插入到该位置后

6、反复步骤2~5

 

与选择排序一样,当前索引左边的全部元素都是有序的。但它们的终于位置还不确定,为了给更小的元素腾出空间。它们可能会被移动。可是当索引到达数组的右端时,数组排序就完毕了。

插入排序不会訪问索引右側的元素。而选择排序不会訪问索引左側的元素。

和选择排序不同的是,插入排序所需的时间取决于输入中元素的初始顺序。对一个当中的元素已经有序(或接近有序)的数组进行排序。将会比对随机顺序的数组或是逆序数组进行排序要快得多。

 

【图例】

 使用插入排序为一列数字进行排序的过程。



从前到后逐步有序。


void sort(Comparable[] a)
{
    //将a[]按升序排列
    int N = a.length ;
    for (int i = 1; i < N; i++)
    {
        //将a[i]插入到a[i-1]、a[i-2]、a[i-3]……之中
        for (int j=i; j>=1 && less(a[j], a[j-1]); j--)
            exch(a, j, j-1) ;
    }
}

//在索引i由左向右变化的过程中,它左側的元素总是有序的,所以当i到达数组的右端时排序就完毕了。

改进:要大幅提高插入排序的速度并不难,仅仅须要在内循环中将较大的元素都向右移动而不总是交换两个元素(这样訪问数组的次数就能减半)。

 

【平均情况下插入排序须要~N2/4次比較以及~N2/4次交换。】

【当倒置(两元素颠倒)的数量非常少时。插入排序非常可能比其它不论什么排序算法都要快!

 

【插入排序对于部分有序的数组十分高效,也非常适合小规模数组。它也是高级排序算法的中间过程。】


【插曲】 

Knuth高爷爷说:“虽然第一个二分查找程序于1946年就已经发布了,可是第一个没有bug的二分查找程序在1962年才出现。”

显然,我们上面的二分查找代码是有bug的。

 (今年的阿里巴巴实习生笔试 就出了一个二分查找的改错题~)

至于二分查找,以后会再起一篇博文具体讨论。



希尔排序


希尔排序。也称递减增量排序算法,是插入排序的一种更高效的改进版本号。

希尔排序是基于插入排序的下面两点性质而提出改进方法的:

1、插入排序在对差点儿已经排好序的数据操作时,效率高。 即能够达到线性排序的效率

2、对于大规模乱序数组插入排序非常慢,由于它仅仅会交换相邻的元素。因此元素仅仅能一点一点地从数组的一端移动到还有一端。

 

希尔排序简单地改进了插入排序。交换不相邻的元素以对数组的局部进行排序,并终于用插入排序将局部有序的数组排序。(先将整个大数组基本有序,再对大数组来一次插入排序)

 

思想:使数组中随意间隔为h的元素都是有序的。

这种数组被称为h有序数组。在进行排序时。假设h非常大。我们就能将元素移动到非常远的地方。为实现更小的h有序创造方便。



我们仅仅须要在插入排序的代码中将移动元素的距离改为h就可以。这样,希尔排序的实现就转化为了一个类似于插入排序但使用不同增量的过程。

 

【图例】

 

void sort(Comparable[] a)
{
    //将a[]按升序排列
    int N = a.length ;
    int h = 1 ;
    
    //依据数组长度 选取适当的初始间隔h
    while (h < N/3)
        h = 3*h + 1 ;  //1,4,13,40,121,364,1093....
    
    //每次Shell排序的h逐渐减小
    while (h >= 1)
    {
        //将数组变为h有序
        for (int i = h; i < N; i++)
        {
            //将a[i]插入到a[i-h]、a[i-2*h]、a[i-3*h]……之中
            for (int j=i; j>=h && less(a[j], a[j-h]); j-=h)
                exch(a, j, j-h) ;
        }
        h = h/3 ;
    }
}

希尔排序更高效的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都非常短,排序之后子数组都是部分有序的,这两种情况都非常适合插入排序。

 

希尔排序的算法性能不仅取决于h,还取决于h之间的数学性质。

在实际应用中,使用3*h+1的递增序列基本就足够了。

【在最坏情况下希尔排序的比較次数和N1.5成正比】

 

希尔排序的代码量非常小,且不须要使用额外的内存空间。假设你须要解决一个排序问题而又没有系统排序函数可用(比如执行于嵌入式中的代码)。能够先用希尔排序,然后再考虑是否值得将它替换为更加复杂的排序算法。

 


归并排序


思想:要将一个数组排序。能够先(递归地)将它分成两半分别排序,然后将结果归并起来。

 

【图例】

一个归并排序的样例:对一个随机点的链表进行排序。



void Sort(Comparable[] a)
{
    aux = new Comparable[a.length] ; //分配辅助空间
    sort(a, 0, a.length - 1) ;
}

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) ; //归并结果
}
//原地归并
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 (aux[j] < aux[i])
            a[k] = aux[j++] ;
        else
            a[k] = aux[i++] ;
}

改进:用不同的方法处理小规模问题能改进大多数递归算法的性能。由于递归会使小规模问题中方法的调用过于频繁。所以改进对它们的处理方法就能改进整个算法。(即增大递归的粒度,使递归在达到小范围时停止。而不是到一个元素时停止递归)

对排序来说,插入排序非常easy,因此非常可能在小数组上比归并排序更快。

使用插入排序处理小规模的子数组一般能够将归并排序的执行时间缩短10%~15%。

 


自底向上的归并排序


递归实现的归并排序是算法设计中分治思想的典型应用。

我们能够把递归方式写成迭代的——先归并那些微型数组。然后再成对归并得到的子数组。

首先我们进行的是两两归并,然后是四四归并,然后是八八归并。一直下去。
void sort(Comparable[] a)
{
    aux = new Comparable[a.length] ; //分配辅助空间
    int N = a.length ;
    
    for (int sz = 1; sz < N; sz = sz+sz)         //sz子数组大小
        for (int lo = 0; lo < N-sz; lo += sz+sz)  //一趟归并
            merge(a, lo, lo+sz-1, min(lo+sz+sz-1, N-1)) ;
}

自底向上的归并排序比較适合用链表组织的数据。

这样的方法仅仅须要又一次组织链表链接就能将链表原地排序(不须要创建不论什么新的链表结点)

 

【归并排序是一种渐进最优的基于比較排序的算法】

(即:归并排序在最坏情况下的比較次数和随意基于比較的排序算法所需的最少比較次数都是~NlgN)

 

归并排序的缺点:它所需的额外空间和N成正比。

 


高速排序


思想:高速排序是一种分治的排序算法。它将一个数组分成两个子数组。将两部分独立地排序。

一般策略是先任意地取a[lo]作为切分元素,即那个将会被排定的元素,然后我们从数组的左端開始向右端扫描直到找到一个大于等于它的元素,再从数组的右端開始向左扫描直到找到一个小于等于它的元素。

这两个元素显然是没有排定的。因此我们交换它们的位置。如此继续,我们就能够保证左指针i的左側元素都不大于切分元素,右指针j的右側元素都不小于切分元素。当两个指针相遇时。我们仅仅须要将切分元素a[lo]和左子数组最右側的元素(a[j])交换然后返回j就可以。


 

void  sort(Comparable[] a, int lo, int hi)
{
    if (hi <= lo)
        return ;
    int  j = partition(a, lo, hi) ; //切分
    sort(a, lo, j-1) ;          //将左半部分排序
    sort(a, j+1, hi) ;          //将右半部分排序
}
//高速排序的切分
int  partition(Comparable[] a, int lo, int 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 (j == lo) break ;
        if (i >= j)
            break ;
        exch(a, i, j) ;
    }
    exch(a,lo,j);
    return j ;
}

高速排序的特点是原地排序(仅仅须要一个非常小的辅助栈),且将长度为N的数组排序所需的时间和NlgN成正比。

缺点:在切分不平衡时这个程序可能会极为低效。

 

改进:切换到插入排序

和大多数数组递归排序算法一样,改进高速排序性能的一个简单办法基于下面两点:

对于小数组。高速排序比插入排序慢。

由于递归,高速排序的sort()方法在小数组中也会调用自己。

因此。在排序小数组时应该切换到插入排序。

将 sort()中的语句 if (hi <= lo)  return ;

替换成:          if (hi <= lo + M)  { Insertion.sort(a, lo, hi);  return; }

 


●三向切分的快排


在实际应用中常常会出现含有大量反复元素的数组

在有大量反复元素的情况下。高速排序的递归性会使元素所有反复的子数组常常出现。这就有非常大的改进潜力,将当前实现的线性对数级的性能提高到线性级别。

一个简单的想法是将数组切分为三部分,分别相应小于、等于和大于切分元素的数组元素。

 


void sort(Comparable[] a, int lo. int hi)
{
    if (hi <= lo)
        return ;
    int lt = lo, i = lo+1, gt = hi ;
    Comparable v = a[lo] ;
    
    while (i <= gt)
    {
        if (a[i] < v)
            exch(a, lt++, i++) ; //a[i]比v小 把a[i]值放入[lo...lt-1]集合中
        else if (a[i] > v)
            exch(a, i, gt--) ;   //a[i]比v大 把a[i]值放入尾部
        else
            i++ ;
    }
    
    sort(a, lo, lt-1) ;
    sort(a, gt+1, hi) ;
}

这段排序代码的切分可以将和切分元素相等的元素归位,这样它们就不会被包括在递归调用处理的子数组之中。

对于存在大量反复元素的数组,这样的方法比标准的高速排序的效率高的多。

 


优先队列


很多应用程序都须要处理有序的元素,但不一定要求它们所有有序。或是不一定要一次将它们排序。非常多情况下我们会收集一些元素,处理当前键值最大的元素。然后再收集很多其它的元素,再处理当前键值最大的元素,如此这般

在这样的情况下,一个合适的数据结构应该支持两种操作:删除最大元素和插入元素。这样的数据类型叫做优先队列

 

数据结构二叉堆可以非常好地实现优先队列的基本操作。

当一颗二叉树的每一个结点都大于等于它的两个子结点时,它被称为堆有序。(大顶堆)

我们使用全然二叉树来表达二叉堆。会变得特别方便。全然二叉树仅仅用数组而不须要指针就能够表示。详细方法就是将二叉树的结点依照层级顺序放入数组中,根结点在位置1,它的子结点在位置2和3。而子结点的子结点则分别在位置4、5、6和7。以此类推。


堆的算法


我们用长度为N+1的私有数组pq[]来表示一个大小为N的堆,我们不会使用pq[0]。堆元素放在pq[1]至pq[N]中。

 

在有序化的过程中我们会遇到两种情况

当某个结点的优先级上升(或是在堆底增加一个新的元素)时,我们须要由下至上恢复堆的顺序。

当某个结点的优先级下降(比如。将根结点替换为一个较小的元素)时。我们须要由上至下恢复堆的顺序。

 

·由下至上的堆有序化(上浮)


假设堆的有序状态由于某个结点变得比它的父结点更大而被打破。那么我们就须要通过交换它和它的父结点来修复堆。

交换后。这个结点比它的两个子结点都大。但这个结点仍然可能比它如今的父结点更大。我们能够一遍遍地用相同的办法恢复秩序,将这个结点不断向上移动直到我们遇到了一个更大的父结点。

(溯流而上)

 

void swim(int k)
{
    while (k > 1 && less(k/2, k))
    {
        exch(k/2, k) ;
        k = k/2 ;
    }
}

·由上至下的堆有序化(下沉)


假设堆得有序状态由于某个结点变得比它的某子结点更小而被打破了,那么我们能够通过将它和它的两个子结点中的较大者交换来恢复堆。

交换可能会在子结点处继续打破堆的有序状态,因此我们须要不断地用同样的方式将其修复,将结点向下移动直到它的子结点都比它更小或是到达了堆的底部。

(顺流而下)

 

void sink(int k)
{
    while (2*k <= N)
    {
        int j = 2*k ;
        if (j < N && less(j, j+1))
            j++ ;
        if (!less(k, j))
            break ;
        exch(k, j) ;
        k = j ;
    }
}



堆排序


我们能够把随意优先队列变成一种排序方法。

将全部元素插入一个查找最小元素的优先队列,然后再反复调用删除最小元素的操作来将它们按顺序删去。

 

1.堆的构造

由N个给定的元素构造一个堆,从右至左用sink()下沉函数构造子堆。開始时我们仅仅须要扫描数组中的一半元素,由于我们能够跳过大小为1的子堆。由此向前对每一个结点sink(),直到我们在位置1上调用sink()方法,扫描结束。

(用下沉操作由N个元素构造堆仅仅须要少于2N次比較以及少于N次交换)

(假设我们从左至右用swim()上浮操作遍历数组,则须要用NlogN成正比的时间完毕)

 

for (int k = N/2; k>= 1; k--)

    sink(a, k, N) ;

 

2.下沉排序

堆排序的主要工作都是在第二阶段完毕的。这里我们将堆中的最大元素删除,然后放入堆缩小后数组中空出的位置

这个过程和选择排序有些类似(一步一步选出最值),但所需的比較要少的多,由于堆提供了一种从未排序部分找到最大元素的有效方法。

 

while (N > 1)

{

    exch(a, 1, N--) ; //把堆尾结点与堆顶最大元素交换

    sink(a, 1, N) ;   //对改变后的堆顶结点下沉操作

}

 

堆排序总代码:(仅使用下沉操作)

void sort(Comparable[] a)
{
    int n = a.length ;
    
    for (int k = N/2; k >= 1; k--)
        sink(a, k, N) ;

    while (N > 1)
    {
        exch(a, 1, N--) ; //把堆尾结点与堆顶最大元素交换
        sink(a, 1, N) ;  //对改变后的堆顶结点下沉操作
    }
}

特点:堆排序是我们所知的唯一可以同一时候最优地利用空间和时间的方法——在最坏的情况下它也能保证使用~2NlgN次比較和恒定的额外空间。

当空间十分紧张的时候(比如在嵌入式系统)它非常流行,由于它仅仅用几行就能实现较好的性能。但现代系统的很多应用非常少使用它。由于它无法利用缓存。其数组元素非常少和相邻的其它元素进行比較。因此其缓存未命中的次数要远远高于大多数比較都在相邻元素间进行的算法,如高速排序、归并排序,甚至是希尔排序。

 

应用:

TopM问题

在某些数据处理的样例里,总数据量太大,无法排序(甚至无法所有装进内存)。假设你须要从10亿个元素中选出最大的十个,你真的想把一个10亿规模的数组排序吗?但有了优先队列,你就仅仅用一个能存储十个元素的队列就可以。

 

【例】

100w个数中找出最大的100个数。

答:

先把这100W个数分别放在100个文件里(每一个文件存放1W个数)。

再用优先队列:在每一个文件里求出TOP100,能够採用包括100个元素的堆完毕(TOP100小。用最大堆,TOP100大。用最小堆,比方求TOP100大,我们首先取前100个元素调整成最小堆,假设发现,然后扫描后面的数据,并与堆顶元素比較,假设比堆顶元素大,那么用该元素替换堆顶,然后再调整为最小堆。

最后堆中的元素就是TOP100大)。

求出每一个文件里的TOP100后,然后把这100个文件的TOP100组合起来,共1W个数据,再利用上面类似的方法求出TOP100就能够了。

 


[注]大家都说那动态图NB,那动态图是从维基百科Copy下来的 :-) 

其它部分的图文皆为自己整合总结而来。


下半部分:键索引计数法、基数排序、桶排序、位示图、败者树。




Oh,My God。 居然有人转了此文,而我反倒被举报抄袭,楼主世界观尽毁。

http://www.2cto.com/kf/201405/303623.html 红黑联盟真无耻。 (大家细致看图片中的水印即知,版主勿要轻信于人)


初入CSDN的菜鸟须要大家的爱护~快哭了


====================

(PS:这篇文章非常一般,真不知道有什么好的,可能是它发的时间点对了)

其它有几篇本博主花费了非常多精力总结的文章反而甚少有人关注:   打个广告   希望有兴趣的网友能临幸它们:-) 指出当中的不足。


http://blog.csdn.net/yang_yulei/article/details/26066409 (这个讲红黑树的。绝对浅显易懂。自己画的非常多图例。是读《算法》一书。总结整理而来)


http://blog.csdn.net/yang_yulei/article/details/8086934 (关于C语言的一些隐晦的 易忽略的地方, 若是此文中的题目对你都毫无压力,那你对C是有足够的理解了)


http://blog.csdn.net/yang_yulei/article/details/22529437 (此文是对处理器体系结构原理的一个简介, 作为程序猿多了解一些底层硬件的原理。呃。其有用处不大,纯属好奇)


http://blog.csdn.net/yang_yulei/article/details/24142743 (操作系统内存管理的。 其是之后两篇文章的总领,那三篇文章,把OS内存管理基本上讲的比較仔细了, 但当中有一些个人的理解,比方Linux 0.12和Linux 2.x 的内存管理确实是那样的差异么?   希望有网友指正。)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值