排序

初级排序:选择、插入、希尔

考虑:比较和交换的数量,访问数组的次数,内存开销【原地和副本】和运行时间

NXN的表格,对角线数据

 

常数:1,普通语句,2个数相加

对数:logN,二分策略,二分查找

线性:N,循环,找出最大的元素

线性对数:NlogN,分治,归并排序

平方:N2,双层循环,检查所有的元素对、

立方:N3,三层循环,检查所有的三元组

指数级别:2N,穷举查找,检查所有的子集

选择排序

原理:不断选择剩下元素中的最小者。

首先,找到数组中最小的那个元素;其次,将它和数组中的第一个元素交换位置。如果第一个元素就是最小元素那么它就和自己交换位置;再次,在剩下的元素中找到最小的元素,将它和第二个元素交换位置。如此反复,直到整个素组排序。

分析:对于长度为N的数组,选择排序需要大约1/2N2比较和N次交换。

0到N-1的任意i都会进行一次交换和N-1-i次比较。

特点:运行时间和输入无关,数据移动是最少的。

选择排序N次交换数据,没有更好的利用输入的初始态。

 

插入排序

原理:将元素不断插入到有序序列中。

与选择排序一样,当前索引左边的元素都是有序的,但他们的最终位置都是不确认的,为了给更小的元素腾出空间,他们可能会被挪动。当索引到达数组的最右端就完成排序。

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

分析1:对于随机排序长度为N且主键不重复的数组,平均情况下插入排序需要大约1/4N2次比较以及1/4N2交换。最坏情况下,需要大约1/2N2次比较以及1/2N2交换,最好情况下,需要N-1次比较和0次交换。

分析2:倒置是指数组中两个顺序颠倒的元素。如EXAMPLE中有11对倒置:EA,XA,XM,XP,XL,XE,ML,ME.PL,PE,LE。如果数组中倒置的数量大于数组大小的某个倍数,我们称之为部分有序数组。插入排序中需要交互的操作和数组中倒置的数量相同,需要比较的次数大于倒置的数量,小于等于倒置的数量加上素组的大小再减一。

每次交换数据都改变了2个顺序颠倒元素的位置,相当于减少了一对倒置的顺序,当倒置的数量为0,也就是完成了排序。

每次交换都对应着一次比较,且1到N-1之间的每个i都可能需要一次额外的比较(在a[i]没有达到数组的左端时)。

特点:对于部分有序的数组十分高效。

数组中每个元素距离它的最终位置都不是很远。一个有序的大数组接一个小数组。

数组中只有几个元素的位置不正确。

 

选择排序&&插入排序

特点:选择不会访问索引左侧的元素,插入不会检索右侧的元素。

因为插入不会移动比被插入元素更小的元素,它所需的比较次数平均只有选择排序的一半。

特点:对于随机排序的无重复主键的数组,插入排序和选择排序的运行时间都是平方级别,两者之间的比应该是一个较小的常数。

插入排序比插入不会移动比被插入元素更小的元素,它所需的比较次数平均只有选择排序的一半。

 

希尔排序

原理:使数组中任意间隔为H的元素都是有序的,再使用插入排序法排序。

对于大规模乱序数组的插入排序很慢,因为它只会交换相邻的元素,因此元素都是一点一点从数组的一端移动到另一端。如果主键最小的元素正好在数组的尽头,要将他挪到正确的位置就需要N-1次移动。希尔排序为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。

分析:递增序列1/2(3k-1).

实现希尔排序的一种方法是对于每个H,用插入排序将H个子数组独立地排序。希尔排序的实现就转化为一个类似于插入排序但使用不同增量的过程。一个H有序数组就是H个相互独立的有序数组编织在一起组成的一个数组。

使用递增序列1,4,13,40……的希尔排序所需的比较次数不会超出N的若干倍乘以递增序列的长度。

特点:子数组的规模和有序性。递增序列的选择。N3/2

它的运行时间达不到平方级别。在最坏的情况下,它的比较次数和N3/2成正比。

 

归并排序

归并:将两个有序的数组归并为一个更大的有序数组。

归并排序:将一个数组分成两半分别排序,然后将结果归并起来。

特点:保证将任意长度为N的数组排序所需的时间和NlogN成正比。

缺点:所需的额外的空间和N成正比。

分治思想:将一个大问题分割为小问题分别解决,再用所有小问题的答案来解决整个大问题。

归并的抽象方法

将所有元素赋值到AUX[]中,然后再归并回A[]中。在归并时考虑4个判断条件:左边的用尽取右边的元素,右边的用尽取左边的,右边的的当前元素小于左边的当前元素取右边的元素,右边的当前元素不小于左边的当前元素取左边的元素。

public static void merge(Comparable[] a,intlo, int mid, int hi) {

         inti= lo; j= mid+1;

         for(intk = lo; k <= hi; k++) {

                  aux[k]= a[k];

         }

         for(intk = lo; k <= hi; k++) {

                  if(i> mid) a[k] = aux[j++];

                  elseif(j>hi) a[k] = aux[i++];

                  elseif(aux[i]>=aux[j]) a[k] = aux[j++]

                  elsea[k] = aux[i++];

         }

}

自顶而下的归并排序

public class Merge {

         privatestatic Comparable[] aux;

         publicstatic void sort(Comparable[] a) {

                  aux= new Comparable[a.length];

                  sort(a,0,a.length-1);

         }

         privatestatic void sort(Comparable[] a,int lo, int hi) {

                  if(lo>hi)return;

                  intmid = lo + (hi-lo)/2;

                  sort(a,lo,mid);

                  sort(a,mid+1,hi);

                  merge(a,lo,mid,hi);

         }

}

递归:树状图。

每个节点都表示为一个sort()方法通过merge()方法归并而成的字数组。这棵树正好有n层,对于0到n-1之间的任意k,自顶向下的第k层有2k个子数组,每个数组的长度为2n-k,归并为最多需要2n-k,因此每层的比较次数为2k X 2n-k =2n,n层总共为n2n=NlgN。

分析1:对于长度为N的任意数组,自顶向下的归并排序需要1/2NlgN和NlgN次比较。

令C(N)表示将一个长度为N的数组排序时所需要的比较次数。C,0)=C(1)=0,对于N>0,通过递归的sort()方法我们可以由相应的归纳关系得到比较次数的上限:

C(N)<= C(N/2)+ C(N/2)+N   (sort+sort+merge)

右边的第一项和第二项为排序所用的次数,第三项为归并所有的比较次数。因为归并所需的比较次数最少为N/2,比较的下限:

C(N)>= C(N/2)+ C(N/2)+ N/2   (sort+sort+merge)

当N=2n且等号成立时我们能够得到一个解。因为N/2=2n-1,可以得到:

C(2n)= C(2n-1)+ C(2n-1)+ 2n

将两边同时除以2n,则有:

C(2n)/2n =2 C(2n-1)/ 2n + 1

C(2n)/2n =C(2n-1)/ 2n-1 + 1= C(2n-2)/ 2n-2+ 2=…. =C(20)/ 20 + n

C(2n)/2n = C(20)/ 20 + n

C(2n)= C(20)/ 2n + n2n

C(20)=C(1)=0

C(N)=C(2n)= n2n=NlgN

 

分析2:对于长度为N的任意数组,自顶向下的归并排序最多需要6NlgN次访问数组。

每次归并最多需访问数组6N次,其中,2N用来复制,2N用来将排好序的数组元素移动回去,另外组多需要2N次比较。?

特点:归并所需的排序时间和NlgN成正比。

其他:归并排序中的递归会使小规模问题中的调用过于频繁,对于小规模排序使用插入排序可能更快。测试中加入判断是否有序,如果a[mid]小于等于a[mid+1],则数组是有序的,可以跳过merge。不将元素复制到辅助数组,可以节省将数组元素复制到用于归并的辅助数组所用的时间,一种是数据从输入数组排序到辅助数组,一种将数据从辅助数组排序到输入数组。

 

自底向上的归并排序

public class MergeBU {

       private static Conpareble[]aux;

       public static voidsort(Conpareble[] a) {

              int N = a.length;

              aux = newComparable[N];

              for(int sz=1;sz<N;sz=sz+sz) //sz字数组的大小

                     for(intlo=0;lo<N-sz;lo+=sz+sz) //lo:字数组的索引

                            merge(a,lo,lo+sz-1;Math.min(lo+sz+sz,N))

       }

}

特点:对于长度为N的任意数组,自底向上的归并需要1/2NlogN到NlogN次比较,最多访问数组6NlogN次。

处理一个数组的遍数正好是[lgN](2n<=N2n+1中的n),每一遍访问数组6N次,比较次数在N/2和N之间。

特点:比较适合链表,重新组织链表就能将链表原地排序。

 

排序算法的复杂度

特点:没有任何基于比较的算法能够保证使用少于lg(N!)~NlgN次比较将长度为N的数组排序。

二叉树来表示所有的比较。

N个不同的主键有N!中不同的排列,这棵树则有至少N!个叶子节点,如果叶子节点少于N!,则说明有一些排序遗漏了。

从根节点到叶子节点的一条路径上的内部节点的数量就是某种输入下算法进行比较的次数,我们比较感兴趣的就是路径的长度,也就是树的高度。

二叉树是的一个基本组合学性质就是高度为h的树最多有2h个叶子节点,拥有2h个叶子节点的树是完美平衡的,为完全树。

 

快速排序

可能是应用最广泛的排序算法,实现简单适用于各种不同输入数据且在一般应用中比其他算法快很多。

原地排序(只需要一个很小的辅助栈),将长度为N的数组排序所需的时间和NlgN成正比。

它的内循环比大多数排序算法都要短小,但是非常脆弱,要非常小心才能避免低劣的性能。

基本算法

快速排序是一种分治的排序算法。他将一个数组分成两个字数组,将两个部分独立地排序。

快速排序和归并排序是互补的:归并排序将数组分成两个字数组分别排序,并将有序的子数组归并以整个数组排序;快速排序将数组排序的方式则是当两个字数组都有序时整个数组也就自然有序了。在第一种情况中,递归调用发生在处理数组之前,在第二种情况中,递归调用发生在处理整个数组之后。在归并中,一个数组被等分为两半,快速排序,切分的位置取决于数组的内容。

public class Quick {

         publicstatic void sort(Comparable[] a) {

                  StdRandom.shuffle(a);  //消除对输入的依赖

                  sort(a,0,a.length-1);

         } 

         privatestatic void sort(Comparable[] a,int lo,int hi) {

                  if(hi<= lo) return;

                  sort(a,lo,j-1);

                  sort(a,j+1,hi);

         }

}

快速排序递归地将字数组a[lo…hi]排序,先用parition()方法将a[j]放在一个合适的位置,然后再用递归调用将其他位置的元素排序。

该方法的关键在于切分,这个过程使得数组满足三个条件:

1.     对于某个j,a[j]已经排定;

2.     a[lo]到a[j-1]中的所有元素都不大于a[j];

3.     a[j+1]到a[hi]中的所有元素都不小于a[j]。

我们总是通过递归来调用切分进行排序。

因为过程总是能排定一个元素,用归纳法我们不难证明:如果左右子数组都是有序的,那么左子数组(有序且小于等于切分元素)、右子数组(大于等于切分元素)和切分元素组成的结果也是有序的。

切分:一般随意地取a[lo]作为切分元素,从数组的右边开始向左扫描,找出大于等于它的元素,再从左边开始向右扫描找出小于等于它的元素,将这两个找出的元素交换,一直下去直到这两个指针i和j相遇为止,再将a[j]与a[lo]交换,并返回j。

private static int parition(Comparable[]a,int lo,int hi) {

         inti = lo,j= hi+1;

         Comparablev = a[lo];

         while(true){

                  while(a[++i]<=v)if(i == hi) break;

                  while(a[--j]>=v)if(j == lo) break;

                  if(i>=j)break;

                  exch(a,i,j);

         }

         exch(a,i,j);

         returnj;

}

原地切分

如果使用一个辅助数组,很容易实现切分,但是切分后的数组复制回去的开销也很大,一个初级的程序员往往可能会在空数组创建在递归的切分方法中,这样大大降低排序的速度。

不要越界

如果切分元素是数组中最大或者最小的那个元素,那么就要小心被让扫描指针跑出数组的边界。测试条件(j == lo)是多余的。

保持随机性

终止循环

处理切分元素有重复的情况

终止递归

 

性能特点分析

快速排序最好的情况是每次都正好能将数组对半分。在这种情况下快速排序所用的比较次数正好满足分治递归的CN=2CN/2+N公式。2CN/2表示2个字数组排序的成本,N表示切分元素和所有数组进行比较的成本。根据归并排序的分析,解为CN~NlgN.

如果使用一个辅助数组,

分析1:对于长度为N的任意无重复数组,快速排序平均需要~2NlnN次比较以及1/6的交换。

分析2:对于长度为N的任意无重复数组,快速排序最多需要N2/2次比较。

 

算法改进

分析1:切换到插入排序。

和大多数递归排序算法一样,改进快速排序性能的简单方法基于以下:

1.     对于小数组,快速排序比插入排序慢;

2.     因为递归,快速排序的sort()方法在小数组中也要调用自己。

Sort()中方法:

if(hi <= lo) return;

改为:

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

转换参数M的最佳值与系统有关。M =(5-15)

分析2:三取样切分。

改进性能的第二个办法是使用子数组的一小部分元素的中位数来切分数组。人们发现将取样大小设为3并用大小居中的元素切分的效果最好。

分析3:熵最优排序。

实际中会出现大量的重复元素的数组,例如按照性别区分。改进性能的一个简单方法是将数组切分为三部分,分别对应于小于、等于和大于切分元素的数组元素。例如荷兰国旗问题。

优先队列

很多程序需要处理有序的元素,但是不一定要求他们全部都有序,或者不一定一次性全部有序。很多情况下我们会收集一些数据,处理当前键值最大的元素,然后再收集更多的元素,再处理当前最大的键值。例如,绝大多数手机分配给来电的优先级都比游戏高。

在这种情况下,一个合适的数据结构应该支持两种操作:删除最大的元素和插入元素。这种数据结构就是优先队列。优先队列的使用和队列(删除最老的元素)以及栈(删除最新的元素)类似,但高效实现更具挑战性。

队列最重要的操作:删除最大元素delMax()和插入元素insert()。

 

Questions:

输入N个字符串,每个字符串都对应着一个整数,从中找出最大或者最小的M个整数及其关联字符串。

从N个输入中找出最大的M个元素所需的成本

示例

时间

空间

排序算法

NlogN

N

调用基于初级实现的优先队列

NM

M

调用基于堆实现的优先队列

NlogM

M

 

初级实现

Ø  数组实现(无序)

下压栈

Ø  数组实现(有序)

Ø  链表表示法

 

堆的定义

在二叉堆数组中,每个元素都要保证大于等于另外特定位置的两个元素,同时这些位置的元素又至少要大于等于数组中的两外两个元素,以此类推。

定义:当一颗二叉树的每个结点都大于等于它的两个子节点,它被称为堆有序。根节点最大。

二叉堆表示法:如果我们用指针来表示二叉堆有序的二叉树,那么每个元素都需要三个指针来找到它的上下结点(父节点和两个子节点个需要一个)。完全二叉树只用数组而不用指针就可以表示。具体的方式就是将二叉树的结点按照层级顺序放入数组中,根节点在位置1,它的子节点在2,3,而子节点的子节点就分别在4,5,6,7.【([k/2]=[(k+1)/2]),将位置不设为0,根据根节点子节点很容易相互找到位置】。在位置为K的结点,父节点为[k/2],而它的两个子节点分别为2k和2k+1。一颗大小为N的完全二叉树高度为[lgN]。

 

堆的算法

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

//比较

private boolean less(int i,int j) {

         returnpq[i].comparableTo(pg[j])<0;

}

//交换

private void exch(int i,int j) {

         keyt = pq[i];

         pq[j]= pq[i];

         pq[i]= 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){

                  intj = 2*k;

                  if(j<N&& less(j,j+1)) j++;

                  if(!less(k,j))break;

                  exch(i,j);

                  k= j;

         }

}

插入元素:我们将新元素加到数组末尾,增加堆的大小并让这个新元素上浮到合适的位置。

删除最大元素:我们从数组顶端删去最大的元素并将数组的最后一个元素放到顶端,减小堆得大小并让这个元素下沉到合适的位置。

分析1:对于一个含有N个元素的基于堆的优先队列,插入元素操作只需不超过(lgN+1)次比较,删去最大元素的操作最多需要2lgN次比较。

两种操作都是在根节点和堆底之间移动位置,而路径的长度不超过lgN。对于路径上的每个结点,删除最大元素需要两次比较(除了堆底元素),一次用来找出较大的子结点,一次用来确定该子节点是否需要上浮。

分析2:d叉堆。logdN

分析3:索引优先队列

多向归并问题:将多个有序的输入流归成一个有序的输出流。输入可能来自多种传感器的输出。

 

堆排序

堆的根节点,也就是a[1]为最大值。通过不断取出堆中的a[1]放在末尾,缩小堆的大小,再次下沉得到新的堆。

堆排序分为两个阶段:在堆的构造阶段中,我们将原始数组重新组织安排进一个堆中,将无序的转换为堆,然后再下沉排序阶段,我们从堆中按照递减顺序取出所有元素并得到排序结果。

堆的构造

由N个给定的元素构造一个堆。

1.     从左至右遍历数组,就像连续向优先队列插入元素一样,用上浮swim()保证扫描指针左侧的所有元素已经是一颗堆有序的完全树。

2.     从右至左用下沉sink()函数构造堆。如果一个结点的两个子结点都是堆,那么在该结点上调用sink()可以将它们变成一个堆。这个过程会递归地建立起堆的秩序。开始我们只需要扫描数组中一半的元素,因为我们可以跳过大小为1的子堆,最后我们在位置为1上调用sink()方法,扫描结束。

用下沉操作由N个元素构造堆只需少于2N次比较以及少于N次交换。

下沉排序

将堆中的最大元素删除,然后放入堆缩小后数组中空出的位置。堆提供了一种从末排序部分找到最大元素的有效方法。

 

算法

是否稳定

是否为原地排序

将N个元素排序的复杂度

备注

时间

空间

选择排序

N2

1

 

插入排序

N~N2

1

 

希尔排序

NlogN ?N6/5

1

 

快速排序

NlogN

logN

 

三向快速排序

N~NlogN

logN

 

归并排序

NlogN

N

 

堆排序

NlogN

1

 

 

 

应用

快速排序是最快的通用排序算法。因为它的内循环中的指令很少(还能利用缓存,因为它总是顺序访问数据)。如果排序稳定性很重要而空间又不是问题,归并可能是最好的。

1.     找出重复元素

2.     找出输入流中M个最大的元素

3.     找出一组数中第K小的元素:中位数和顺序统计【基于切分的选择算法法和优先队列】

基于切分的选择算法:N+N/2+N/4+N/8…<2N线性

 


//更新与2014/1229

1.冒泡排序:

基本思路是:每次比较两个相邻的元素,如果他们的顺序错误就把他们交换过来。

核心部分是:双重嵌套循环。时间复杂度O(N*N)。如果有n个元素,进行n-1趟操作,每一趟就是将当前元素进行归位。

2.快速排序:

基本思路是:二分和递归思想。

核心部分是:在每一个子序列中,找一个基准元素,一般把第一个当做基准元素,在新的序列将元素分布基准点左右,大的放在右边,小的放在左边。最坏时间复杂度O(N*N),平均时间复杂度O(NlogN)。在分布过程中,以基准元素为基准,在这个子序列中,从左右两边开始查找,左边大于基准数的和右边小于基准数的2个元素交换,一直到左右2个查找点相遇,接着就是把相遇点和基准元素交换,到此新序列就分布完成。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值