深入浅出排序算法

    最近在忙着准备找工作,对于码农来说,找工作之前必备的就是各种的排序算法,其中包括算法实现、复杂度分析。于是我也开始研究各种排序算法,但是看完几遍之后发现,其原理并不复杂,于是就在思考,这些算法这么重要,那么它们在实际解决问题时如何来使用呢?这篇文章我就个人的理解,尽量形象、简单的描述各种基本排序算法的原理,并对其复杂度进行了相应的分析,最后说明其在实际使用中的应用。我希望我的这篇文章尽量的通俗形象,让我们展开想象的空间吧!

一、算法原理分析:

 1、冒泡排序:

  首先从它的名字上来理解它的原理,假设一个湖底从左到右并排了10(n)个泡泡,我们在湖底从左向右游动,每次只比较相邻的两个泡泡,如果左边的比右边的小则交换其位置,这样游玩了一趟之后,即比较了9(n-1)次,最大的那个就得到了,这时我们将这个泡泡冒出水面,以此类推,我们需要这样重复9(n-1)次即可(考虑最后只剩下两个),每一次需要比较(n-i)次,j为跑了的次数。

伪代码如下:

    for i = 1:n-1 (n-1趟)

      for j = 1:n-i(比较n-i次)

        if a[j] > a[j + 1]

          swap(a[j],a[j+1])

        endif

      endfor

    endfor

当然由于数组元素从0开始,则编程时只需要将循环的首尾均减一即可。

复杂度分析:本算法共有两个循环,外循环共(n-1)个,内循环共(n-i)个,则总数为(n-1)+(n-2)+...+1 = [n*(n-1)]/2=O(n^2).

 2、插入排序:

    本算法也从其名字来理解,这次我们想象一下学生们排队好了,每个学生的身高都不同,现在希望他们能够快速的按照从矮到高的顺序排好队。我们的策略是插入的人都和队伍中的人挨个比,如果比队伍中的人矮,那就将队伍中的人向后移动一个位置,一直到合适的位置将其插入进去。这时候我们假设第一个元素已经在数组中了(因为没人和他比),这时我们只需要将剩下的n-1个人插入到队伍中就好了。假设队伍中已经插入了i个人,则第i+1个人需要和队伍中的i个人比才可以。

  for i = 2:n  //插入第i个人

    key = a[i]; //设其为关键字用来跟每一个位置进行比较

    j = i-1;

    for j = i-1:1 //关键字和前面i-1个人比较

        if a[j] > key //大于关键字则将其向后移动一个,空一个空出来

      a[j+1] = a[j];

     else   //如果key大于队伍中的值时就退出

      break;

     endif

    endfor

    a[j+1] = key;//将其插入到该值的后面

  endfor

复杂度分析:该算法也包含两层循环,外层有n-1个元素,内层有i-1个元素,则共有1+2+...+n-1 = [n*(n-1)]/2 = O(n^2)

 

稍微休息一下,纵观以上两个算法,虽然算法不同,但是都得需要循环比较,在相对有序时,快速排序的内层循环可以快速退出,因此其效果相对好一点,而冒泡排序还是从头比较。

 3、归并排序:

  看了两个循环嵌套的算法,下面看看递归的算法吧。归并排序是典型的具有归并性质的算法。先来说说它的思想,归并,换言之就是合并,既然要合并那就得先把原来的数组分开吧。咱们还是接着上面的例子,我们先将队伍分成两组,然后对两组排序,排序后进行合并。分成的两组其实又可以继续分为两组并合并,因此此处就可以使用递归的方法。

 

Merge://合并的代码

input:a[]

n1 = med;

n2 = n-med;

L[] = a[1:n1];

R[] = a[med+1:n];

L[n1+1] = 65535;//哨兵牌,每次到这之后只将剩下的那一摞放到数组中即可

R[n2+1] = 65535;

 

i = 1:n1;

j = 1:n2;

 for k = 1:n

  if(L[i] < R[j])

    a[k] = L[i]

    i++;

  else

    a[k] = R[j]

    j++;

  endif

endfor

MergeSort:

  q = n/2;

  MergeSort(a,1,q);

  MergeSort(a,q+1,n);

  Merge(a,1,q,n);

复杂度分析:对于递归调用的复杂度分析,要明白主方法:T(n)=aT(n/b)+f(n) 要掌握三种情况:1)如果f(n)比n^(log(b)a)小,则T(n) = Ot(n^(log(b)a));2)如果f(n)比n^(log(b)a)大,则T(n)= ot(f(n));3)如果f(n)和n^(log(b)a)差不多大,则T(n)= ot(n^(log(b)a) * lgn)。

在此算法中,a = 2,b = 2;故 n^(log(b)a) = n = f(n),所以满足3),故T(n)= O(nlgn)。虽然该算法时间很快,但是需要使用两个数组来存储L和R,典型的空间换取时间的手法。

4、快速排序:

  听到这个名字,相必大家很快就觉得这个算法肯定很快,事实上也确实如此。该算法也采用了分而治之以及递归的思想。其主要的思想是:先随机选择一个关键人物作为标准,将比他矮的放到左边,比他高的放到右边。然后再在剩下的两组中按照此种方法进行递归。

Partition://分开确定pivot的位置,方便将其分开递归排序

  pivote = a[1];//将第一个人作为标准

  i = n+1;

  for j = tail:1

    if a[j]>=pivote

     i--;

     swap(a[i],a[j]);

     p_low++;

     endif

     endfor

  swap(a[1],a[i -1]);

return i -1;

QuickSort:

  p = Partition(a,1,n);  

  QuickSort(a,1,p-1);

  QuickSort(a,p+1,n);

复杂度分析:类似于归并排序,该算法也将原问题分成两部分,T(n)=  T(n/p)+T(n/(1-p))+n,其最好情况T(n)=2T(n/2)+n=O(nlgn),最坏的情况是T(n)= T(n-1)+O(n),T(n) = O(n^2);而对于不均分的情况,分析得出T(n)=O(nlgn)。

                该算法虽然和快速排序算法的复杂度一样,但是该算法在基本排好序的情况下属于最坏的情况,因此使用时需谨慎。

5、堆排序:

  下面就来说一说堆排序,堆排序相对于上面几种排序稍微麻烦一点,但是只要掌握其精髓,也是很容易理解的。这个举实际的例子确实不太好举,但是大家可以想象出一棵二叉树。

  堆排序,主要利用了最大堆和最小堆,其本质就是棵二叉树。

  该排序过程大概可以分成如下三步:1)建立一个二叉树  2)将堆变为最大堆(即将最大值转移到堆顶),从最后一个非叶子节点开始  3)将堆顶元素和最后一个元素互换,然后调整堆,使其满足最大堆(堆长度在变)

 1)此处不需要什么操作,只是提醒大家数组中的元素在树中的位置为前序遍历存储(根左右)

2)

BuildMaxHeap(a,n):

  第一个非叶子节点为:n/2

  for i = n/2:1

    HeapAjust(a,i,n);

  end

HeapAjust(a,i):调整为最大堆

  l=LEFT(i);//左子树

  r = RIGHT(i);//右子树

  if l a[i]

    largest = l;

  endif

  if ra[largest]

    largest = r;

  endif

  if largest != i

    swap(a[i],a[largest]);

    HeapAjust(a,largest);

  endif

HeapSort(a,n)

  BuildMaxHeap(a,n);

  for i  = n:2

    swap(a[1],a[i];

    HeapAjust(a,1,i-1);

  endfor

复杂度分析:该算法主要包括两部分,第一部分是堆调整O(lgn),建最大堆时,共调用(n/2)次,故其为O(nlgn).堆排序时主要包括O(nlgn)+ O(nlgn) = O(nlgn)

堆排序不需要大量的递归或者多维的暂存数组。这对于数据量非常巨大的序列是合适的。比如超过数百万条记录,因为快速排序,归并排序都使用递归来设计算法,在数据量非常大的时候,可能会发生堆栈溢出错误。但是快速的时间一般线性增长,但是堆排序为nlgn的速度。

转载于:https://www.cnblogs.com/jiangnanrain/p/4348646.html

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