算法 第四版 学习笔记.

#笔记   #{A}  

作者 Tenie
2017-08-21 00:00:00 字数: 17011 阅读: 27 评论: 0

//数组转阵型

int array[][] = { 

{11,12,13,14,15},

{16,17,18,19,20},

{21,22,23,24,25},

{26,27,28,29,30},

{31,32,33,34,35}

};

for(int k =0;k<4;k++) //转几次

{

int[][] tempArray =new int[5][5]; //存入临时数组

 

//core

for(int i = 0; i<5;i++){

for(int j =0; j<5;j++){

tempArray[i][j]=array[(4-j)][i] ; //第一行第一个,改为第5行,第一个;

  //第一行第2个,改为第4行,第一个;

}

 

array = tempArray;

}   

 

//algs.jar API 

edu.princeton.cs.algs4.In("str") //通过参数String 可以读取本地文件,http; 如果不传参读取标准输入流 

1.4 算法分析

1.4.3  增长数量级的分类

表1.4.7 对增长数量级的常见假设的总结

----------------------------------------------------------------------------------

描述 增长数量级    典型代码 说明 举例

----------------------------------------------------------------------------------

  常数级别   1    a = b + c  普通语句   将两个数相加

  对数级别  logN 二分查找   二分策略    二分查找

  线性级别             N 单循环              循环   找出最大元素

  线性对数级别        NlogN         算法2.4    分治       归并排序

  平方级别     N²(n的平方)    双层循环   双层循环    查找所有元素对

  立方级别 N³ 三层循环   三层循环

  指数级别 (2的幕) 第六章讲   穷举查找 检查所有子集

  

 y

  |指数最接近y

  |立方其次   /

  |平方再次 /

运 |           /

行 | /  线性对数和线性级别在y和x之间45度  

  |       /

时 | /

间 |   /

  | /             常数最接近x,对数级别靠近常数级别

  | ------------------- x

问题规模

1.4.5  设计更快的算法

书中例子找出3个数相加和为零的;3个数有多少对(比较粗糙,使用了3层循环立方级)

3-sum:  

public static int count(int[] a)

{

int N = a.length;

int cnt = 0;

for(int i =0;i<N;i++)

for(int j=i+1;j<N;j++)

for(int k=j+1;k<N;k++)

if(a[i]+a[j]+a[k] ==0)

cnt++;

return cnt;

}

1.4.5.1 热身运动2-sum

public static int count(int[] a)

{

Arrays.sort(a);    //在开始前先给数组排序

int N = a.length;

int cnt = 0;

for(int i =0;i<N;i++)

if(BinarySearch.rank(-a[i],a) > i )  //取i下标的值取反后,在a数组中二分查询,大于i,为了防止重复

cnt++;

return cnt;

}

1.4.5.2 3-sum 问题的快速算法

public static int count(int[] a)

{

Arrays.sort(a); //数组排序

int N = a.length;

int cnt = 0;

for(int i =0;i<N;i++)

for(int j=i+1;j<N;j++)

if(BinarySearch.rank(-a[i]-a[j],a)> j)  //道理一样,前面2个数相加后取反,能在数组中二分查找找到,说明有3-sum; 之所以快因为少了一层循环

cnt++;

return cnt;

}

1.4.6 倍率实验

就是对计算规模加倍测试;如第一输入100,获取计算时间, 第二次输入量加倍如200,获取计算时间,有了2个时间后就能计算倍率, 第一个时间为分母第二时间为分子,计算倍率值;

依次类推等倍率值会规定一个值后, 通过该值就能计算更大规模的计算大概需要的时间了, 而不一定真的直接去计算大规模的运算

在有性能压力的情况下,应该考虑对编写过的所有程序进行倍率实验----这是一种估计运行时间的增长数量级的简单方法.

例子:

main()

{

double prev = timeTrial(125);  //该方法运行程序,结束会返回运行时间(秒);刚开始的运行规模

//程序运行规模加倍(循环加倍运算)

for(int n=250; true; n+=n) //这里n的运算系数是2, 就是每次计算量是前一次量乘2

{

double time = timeTrial(n); //加倍运算

StdOut.printf("计算量:%d ; 运行时间:%f ; 倍率:%f \n",n,time,time/prev);  //输出运算结果

prev = time;

}

 

}

 

评估程序解决大型问题的可行性

计算更大规模的运算, 只要把循环中运算系数2,增大,比如10

 

评估使用更快的计算机所产生的价值

相同运算规模,如果用一台更快的计算机能够加快多少?

一般来说新计算机比老计算机快x倍,远行时间也将为原来的x分之一. (如快了2倍,一样的运行时间为time/2)

 

 

1.5 案例研究:union-find算法 <union 联合>

1.5.1 动态连通性

     下面都是围栏这个话题展开的,例子是:输入2个对象, 如果他们之间没有连接, 就建立连接;2个对象,分别可以和其他对象连接,

此时,如果这些对象中2个对象并没有直接连接, 对象想连接, 可以通过以连接的对象来路由, 不要自己直接建立连接!

图1.5.2

  案例中

  一个p对象 称为:触电,  q对象和p相等(对称的) 称为:连接, 把其他的和p或q相等的(对称的对象)称为:连通分量(or分量,也就是有多少个独立的连接) 

 

案例的目录是找到有多少个连接网络(连通分量);从图片中用肉眼很容易找到这些连通分量, 但是在一堆数据中找到这些连通分量是困难的.

 

表1.5.1 union-find算法的API

-----------------------------------------------------

public class UF 

----------------------------------------------------

UF(int N) 以整数标识(0到N-1)初始化N个触点

void union(int p, int q) 在p和q之间添加一条连接

int find(int p) p所在的分量的标识符(0到N-1)

boolean connected(int p ,int q) 如果p和q存在于同一个分量中则返回true.//就是说它们在一个连接网中就返回true

int count() 连通分量的数量 //有多少个连接网络

--------------------------------------------------------

成员变量

private int[] id; //分量id(以触点作为索引)

private int   count; //分量数量

 

说明:

该类初始化时,初始化一个有多少个触点(对象)的数组

然后接受p和q 触电对! 如果他们不在一个连接分量中, 就在他们之间建立连接;

//第一次输出,肯定是没有连接的,给他们建立连接,

//之后他们,又可以和其他触电建立连接,(在建立连接的时候要判断,新的触点是不是在一个连通分量中,在一个连通分量中就不用连接了)

 

UF算法目的: 计算N个触电之间最后建立了多少个连接分量, 建立了多少条连接

//测试代码

main()

{

int N = StdIn.readInt(); //获取 触电的数量

UF uf = new UF(N); //初始化N个分量

while(!StdIn.isEmpty()    //循环获取输入

{

int p = StdIn.readInt();  

int p = StdIn.readInt();    //读取整数对

 

if(uf.connected(p,q)) continue; //判断它们是不是在一个分量中,在的话忽略

uf.union(p,q);    //归并分量(给p,q 建立连接)

StdOut.println(p +" "+ q); //打印连接;打印出p,q 说明它们之间建立的连接

}

StdOut.println(uf.count()+ "components");   //程序最后打印 有多少个连通分量

}

 

1.5.2.1 quick-find 算法

//p所在的分量的标识符

public int find(int p)

{

//quick-find 算法 

return id[p];

}

 

//给2个触点建立连接, 也就是把2个分量连接(等于是把2个连通分量建立连接,一个触点后面有多个连通的触点,等于把所有的触点都连通了)

public void union(int p,int q)

{

//quick-find 算法 ; 

int pID = find(p); //找到p,q对应id[]数组下标中的值, 下面对值比较,相等说明是一个分量,不相等,不是同分量,建立连接(建立连接就是把p,q的给赋值层相等的(包括分量中所有的触点))

int qID = find(q); 

//如果p和q已经在相同的分量中则不需要建立连接

if(pID == qID) return;

 

//下面是建立连接的思路, 就是把p,q 上的值赋值成相等的

for(int i =0 ; i < id.length; i++) //总结:

{ //     建立的id[]里面的值是相对于的触点的p,q的值, 如:id[0] -> 0 ; id[1]

if(id[i] == pID) //分量连接(触点连接),建立连接就是把数组中值和p相等的值全都改为q的值,

{        //连通分量中的值都是一样的!

id[i] = qID; //p,q要不要建立连接就是比较2个p,q下标对应的值是不是相等,不相等,建立连接(这里的做法是吧所有p下标下的"P值",在数组中所有"p值", 都赋值成"q值")

} //把"p值"赋值层"q值",和把"q值"赋值成"p值也可以";计算量是一样的

} //最后把连通分量的数量减1, 

count --;

}

quick-find算法分析:

find()操作很快, 只要访问一次数组; 但union()方法每次建立连接都要扫描整个数组,给分量赋值的次数又是在1~(N-1)此的操作, 如果数组很大,会比较慢

如果使用quick-find算法来解决动态连通性问题并最后只得到一个连通分量的话,就需要N-1次的union(),即至少(N+3)(N-1)~N的二次方 次的数组访问(这个访问次数是find()2次,循环数组N次,赋值时再一次)

 

1.5.2.3 quick-union 算法

该算法思路:合并两个连通分量的时候, 刚开始的时候,把p作为q的父节点(反之也一样); 后面是根节点直接的操作

union()方法的逻辑变的简单了, 操作也少了,只要该一个值就行了,但在判断p,q是不是在一个分量中需要使用的find()操作就多了,

find()通过传入的值,找到对象的父节点,直到根节点, 如果当前的值是一个值没有父节点,也就没有根, 自己就是一个分量

union()在合并两个分量的时候,首先使用find()找到p,q的根节点, 如果他们的根节点不同, 就不把q根节点作为p的根节点的一个子节点;这样2个分量有了一个根节点,表示他们在同一个分量中

代码:

//找到根

private int find()

{

while(p != id[p]) p = id[p]; //如果[p]下标中的值 和  p的值 不等;就把下标中的值取出来,把取出的值赋值给p, 继续获取新p值[下标]下的值

return p

}

//合并分量 ;合并根

public void union(int p,int q)

int pRoot = find(p);

int qRoot = find(q);  

if(pRoot == qRoot) return;    

id[pRoot] = qRoot; //把q根(下标),赋值成p下标下的值

count --;

}

总结:

把分量的父节点存入分量下标的值里

find() 根据这个值找到他的父(根)节点的下标;如下标的值和 元素值 相等就是根,初始时每个触点都是根

union() 操作就简单了,合并就是把2个分量的根节点,1个变成了另一个的子节点

 

1.5.2.7 加权quick-union-find算法分析

该算法在前一个算法上做了一个小改动, 在合并2个分量时,判断哪个分量的树更小, 把小的合个大的,

好处就是最终的树层次不会很深, 减少了find()方法的循环次数. 复杂度仅仅加了一个数组来记录分量上有多少个触点

class WeightedQuickUnionUF{

private int[] id;

private int[] sz; //记录对应根节点下有多少个分量

private int count;

//构造

public UF_weightedQuickUnion(int N){

count = N;

id = new int[N];

for(int i = 0; i < N; i++)

id[i] = i; 

//初始化记录数量的数组, 初始时, 每个触点就是一个分量,

sz = new int[N];

for(int i = 0; i < N; i++)

sz[i] = 1; // 赋值为1;

}

//查找根节点

public int find(int p)

while( p != id[p]) p = id[p]; //如果p 的值和数组下标[p]的值不同 ,就把该下标的值赋值给p, 然后接着用p做为下标取值和p值比较

return p;

//合并分量

public void union(int p,int q)

int pRoot = find(p);

int qRoot = find(q);  

if(pRoot == qRoot) return;   //

//加权qunick-union;重点在合并的时候,判断哪个树小, 小的合并给大树; 然后记录大树下有多少分量在sz数组中

if(sz[pRoot] > sz[qRoot]){

id[qRoot] = pRoot;

sz[pRoot] += sz[qRoot] ; //把原本2颗树的分量数相加后,就是合并树下有多少个分量

}else{

id[pRoot] = qRoot;

sz[qRoot] += sz[pRoot] ;

count --;

}

}

总结:

weighted-quick-union 算法通过小树合并给大树, 降低了树的深度,减少了find()方法访问数组的次数;让程序已经很快了!

还有一种完全扁平化子节点的方式;(所有子节点都只有一个父节点)代码如下

public int find(int p) {

int root = p;

while (root != id[root])

root = id[root];  //获取根节点

 

//把一个树上的所有子节点都直接连到根节点上

while (p != root) {

int newp = id[p];

id[p] = root;//将触点连接到根节点 ,(把深层次的触点连接到根节点)

p = newp;

}

return root;

}

进find()后, 所有的子节点都挂载到根节点了。

还有一种路径减半的

public int find(int p) { 

while (p != id[p]) { //树的深度减半的方式

id[p] = id[id[p]];    // path compression by halving

p = id[p];

}

return p;

}

在weighted-quick-union算法中 ,用路径压缩这些, 没什么效果

在quick-union算法中, 用路径压缩后, 和weighted-quick-union的效率相似了

 

第二章 排序

排序要用到的比较方法less(v,w),v小于等于w返回true;代码如下:

protected   boolean less(Comparable v, Comparable w){

return v.compareTo(w)<0; //compareTo()返回0 相等,1 大于,-1小于

}

 

2.1.2 选择排序

逻辑最简单的排序

首先在内循环中找到最小的元素, 找到后在外循环中把最小元素和第一个元素交换位置, 外循环进入下次循环找剩下的元素中最小给给第二个元素交换位置;

它的效率最低下,因为内循环每次都要和剩余的元素做比较找最小元素, 哪怕他已经是最小的元素了.代码如下:

public   void sort(Comparable[] a){

System.out.println("selection");

int N = a.length;

for(int i =0 ; i <N; i++){

//将a[i] 和a[i+1..N] 中最小的元素交换

int min = i;

for(int j = i +1;j<N; j++){  //初始时,但第一个值为min, min 后后面的元素对比, 谁小把谁赋值给min,然后接着往后比较,

if(less(a[j],a[min])){ //比较方法

min = j;

}

}

exch(a,i,min); //交换方法 

}

}

2.1.3 插入排序

该算法可抽象的认为打牌是整理牌的时候, 每拿一张牌就把该张牌插入到目前有序的牌中适当位置.

在计算机的实现中, 为了给要插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位.

 

和选择排序比较

类似的是,

排到一定程度,前面的元素的是有序的,但却是不固定,选择排序前面的选项一定是固定的.

不同的是,

选择排序刚开始会遍历所有元素,

插入排序不是偏离元素是移动元素,在极端情况下如果最后一个元素是最小的, 那么整个数组都要移动N-1次

 

选择排序的排序时间是固定的,

而插入排序如果其中的元素是相对有序的话, 元素的移动量会很少,时间是不固定的,最坏情况下,2个算法时间是接近的

代码如下:

public void sort(Comparable[] 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>0 && less(a[j],a[j-1]); j--){ //如循环到中间; 当前元素和前面的元素做比较,如果小了,就交换,

//因为前面的元素都是有有序的,如果第一次比较失败,那么就直接进入外层循环,移动到下一个元素,开始和前面的元素比较

exch(a,j,j-1);

}

优化版;

//插入排序的优化;在内循环中没有使用到exch()方法,exch()方法每次交换都要利用到一个临时变量缓存才能做交换,

//该优化版中自己做了一次缓存,主要就是缓存的次数少了,就一次, 速度就快在这个上面

public  void sort(Comparable[] a){

int N = a.length;

for(int i=1;i<N; i++){

Comparable t = a[i];

int j = i;

while( (j>0) && (a[j-1].compareTo(t))>0 ){

a[j] = a[j-1];

--j;

}

a[j] = t; 

}

}

 

2.1.6 希尔排序

希尔排序的思想是, 使数组中任意间隔数为h 的元素间都是有序的, 这样的数组被称为h有序数组;

换言之: h有序的数组相对于整个原数组,h数组就是,元素中的元素于元素之间间隔h个元素  

代码:

public void sort(Comparable[] a){

int N = a.length;

int h = 1;

while(h<N/3) h = 3*h + 1; //计算该数组可以产生的最大h值 (比较远"的一个元素)

while(h>=1)

{//将数组变为h有序

for(int i = h; i<N; i++)//把i初始为h的值

{//将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;

}

 

 *  代码解释

 *从第一个元素和 "比较远"的一个元素对比-> 交换   ;  //初始时:其实分为2个数组; 对数组各种相同下标的元素比较, 如果前面的元素和后面的比小的话,互换

 *初始的时候,是用一个公式算出一个h值,作为"比较远"的元素

 *(可是认为,h值是间隔原数组元素之间的距离的);

 *初始时比较的是2个元素的各自第一个;通过比较就交换,  // [h] 比较 [0]?  //0是用i变量替换的;

 *然后进入下次循环 h+1 ; [h+1]的元素和 [1]  //[h++] 比较 [1]?  //这里可以写出[h+1] 比较[i++]

 *等h和的值++ 到N 后 ;第一次循环结束, 但此时h值之前的很多元素都还没比较到呢;

 *第二次循环前 h值再次/3 得到一个新的h值;

 *此时刚开始和第一次循环类似,2个元素相同下标的值对比交换

 *但是h值变的小了, 以h值来对原数组来说可以分好几个h间隔的子数组了,(也就是初始时,中间的元素没比较到,因为间隔的减少,中间的开始比较到了) 

 *比较的方式是: 第一个数组和第二个数组,对应下标比较, 完了后, 第2个数组和第3个数组对应下标值的比较;此时第2个数组有和第一个数组对于的值比较

 *也就是说; 比到第3个数组时,第3个数组和第2个数组比较一次后, 第2个数组和第一个数组对于下标也会比较一次.

 *  

 

总之: 间隔系数h的值是 while(h<N/3) h = 3*h + 1; 计算而来, 一般应用已经够用,不需要去研究这个系数值来提升性能了

希尔排序, 因为是原地排序,不需要额外空间,对于中等数组它色速度是可以接受的, 如希尔排序不满足了, 才去考虑在去换更复杂的排序

 

2.2 归并排序

归并的发明:

在把2个已经有序的数组,合并成一个有序的大数组的时候,很快人们根据这个操作发明了一个简单的递归排序算法:归并排序.

开始对一个数组排序的时候;

首先,将他们分成两半分别排序(递归进行);

然后,将结果归并起来.(2个有序的数组,每次取各自的元素出来比较, 大的或小的元素放入临时数组中,接着取一个元素,和但那个小(大)的比较)

归并的优点是,排序的时间是非常稳定的, 任意长度N的数组,所需要的时间和NlogN成正比; 缺点就是需要额外空间和N成正比

代码如下:

归并2个数组成一个数组的方法(归并排序的核心)

/**

*归并排序; 把一个原数组,分2成2个, 遍历 

* @param a 原数组

* @param lo 起始位置

* @param mid 数组的中间位置

* @param hi    数组最后的位置

*/

public  void merge(Comparable[] a, int lo, int mid, int hi)

{//将a[lo..mid] 和啊[mid+1 .. hi] 归并

int i = lo , j = mid+1;

for(int k = lo; k <= hi ; k++){ //将啊[lo..hi] 复制给aux

aux[k] = a[k];

}

for(int k = lo; k <= hi ; k++){ //归并回到a[lo .. hi]

if     (i > mid) a[k] = aux[j++];//i 大于 mid 说明前半个数组没有值了

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

else if(less(aux[j],aux[i])) a[k] = aux[j++];//初始时,会进入这里,2个子数组每个元素对比后加入原数组

else a[k] = aux[i++];

}

sort()方法就相对简单了,如下:

private   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); //经过递归后, 原数组前半部分的数组和后半部分的数组已经有序了, 将他们合并

}

归并排序的优化

1.在递归的sort()中拆分数组归并, 最终会把数组拆分成到一个数组一个元素为止, 显然数组到了这样的细颗粒度的拆分是没有效率可言的.

此时在子数组在小于15时使用插入排序(或选择排序)来对子数组排序,显然比递归的拆分下去效率要高(因为这类简单的排序作用在小数组上速度比归本更快)

思路就是在归并排序时检测下子数组的长度(如小于15),就不用归并了,直接调用插入对子数组排序; 一般而言此时的远行时间缩短10%~15%

自己实现了下,发现没有提升代码如下:

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

//小数组用插入排序;

if(hi<lo+LENGTH){

int k = lo;

//System.out.println(a.length);

Comparable arr[] = new Comparable[hi-lo+1];

for(int i = 0 ;i < arr.length; i++){

arr[i] = a[k];

k++;

}

new Instertion().sort(arr);

int j = 0;

for(int i = lo ;i <=hi; i++){

a[i] = arr[j];

j++;

}

return; 

//将数组a[lo..hi]排序

if(hi <= lo) return ;

int mid = lo+(hi-lo)/2;

sort(a,lo,mid); //把原数组分割后,递归的对他们排序

sort(a,mid+1,hi); 

if(a[mid].compareTo(a[mid+1])>0)

merge(a,lo,mid,hi); //经过递归后, 原数组前半部分的数组和后半部分的数组已经有序了, 将他们合并

}

 

 

 

2.还可以加一个判断条件来优化, 如果a[mid] 小于a[aid+1],就认为该数组已经是有序的就跳过merge()方法,不影响sort()方法的递归调用,但是任意

有序的子数组算法的运行时间就变成线性的了;性能的提升只有在数组非常庞大时才有点效果, 因为该判断只有在数组中只有2个元素的子元素是才会有优化;代码如下:

private   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); 

if(a[mid].compareTo(a[mid+1])>0)

merge(a,lo,mid,hi); //经过递归后, 原数组前半部分的数组和后半部分的数组已经有序了, 将他们合并

}

 

2.2.3 自底向上的归并排序

  从小数组(一个元素的数组)开始归并;先22归并,在44,在88, 循环到x N-1;

  最后一次的归并的时候第2个数组可能比第1个数组要小,实现在MergeBU

效率没有递归的从上往下拆分来的快, 代码如下:

public void sort(Comparable[] a)

{//进行lgN次两两归并

int N = a.length;

aux = new Comparable[N];

for(int sz = 1; sz < N; sz = sz+sz){

for(int lo = 0; lo < N-sz ;lo +=sz+sz){

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

}

}

  不过自底向上的归并可以使用链表来实现,这样就不需要临时数组了,只需要把链表连接改变下即可

 

2.3 快速排序

属于原地排序的算法,不像归并算法需要额外的空间, 归并还是比快排快一点,快排优点就是不需要额外空间的排序算法中,速度是最快的

思路:

1. 打乱数组中元素的顺序(为了防止出现极端情况)

2. 使用数组第一个元素作为分割元素,循环的从数组2端查找小于分割元素和大于分割元素的 元素;

3. 从第二个元素开始往后扫描,找到比分割元素大的(包括等于的)元素(a),

3-1. 从数组末尾开始向前扫描,找到比分割元素小的(包括等于的)元素(b),

3-2. 交换a,b的位置,

3-3. 如果a>=b,那么不用交换a,b的位置了, 分割元素直接和b元素交换,此时的分割元素的前面的元素都比其小,后面的比其大

4. 返回分割元素当前的位置, 递归的对其分割的前后数组排序

代码:

//排序算法

private 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+1..hi]

int i = lo;

int j = hi+1;

Comparable v = a[lo]; //切分元素,初始是在数组的第一个元素的位置, 但和前后元素比较后, 得到2个子数组后,返回该切分元素的位置

 

while(true)

{ //扫描左右, 检查扫描是否结束并交换元素

while(less(a[++i],v)) //从第二个元素开始,找 大于(包括相等)分割元素 的元素下标 i值,找到退出循环

if(i == hi)break; //越界判断

while(less(v,a[--j])) //从最后一个元素开始, 找小于(等于)分割元素的下标j值, 找到退出循环

if(j == lo)break; //越界判断 

if(i >= j)break; //如果大于v的元素 下标>= 小于v元素的下标, 就退出大循环; 在大循环外面直接把v和j交换

exch(a,i,j);

}

exch(a,lo,j); //v 和 当前j的位置对换,经过内循环,已经知道了, j的值 小于v的值

return j; //返回 j这个下标值

}

 

快排的性能改进

1. 在递归过程中,子数组变成较小是如5~15的长度是;可以切换为插入排序;在sort()方法中加判断如下:

原来的判断:

if(hi<=lo) return; 如果第一个元素就是最后一个元素,就结束排序

改成如下:

if(hi<=lo+M){Insertion.sort(a,lo,hi);return;}//M为要改为插入排序时,子数组的长度

2. 三取样切分

 

3. 熵最优的排序

在有大量重复元素的数组中,使用该算法能提高效率,比归并排序还能快一点,归并的时间则是固定的.

但是重复的元素要足够多.

 

思路:

本来数组分为前后2部分,前部分比切分元素小,后面的比切分大,现在分为3部分,增加中间部分,和分割元素一样大的.

原本的实现是那分割元素从前半部分找大于它的元素,后半部分找小于它的元素, 然后交换找到的元素,直到半部分的扫描指针大于等于后半部的扫描指针;

然后把切分元素插入到后半部分扫描的指针的位置;

三向切分: 记录一个后半部分的指针, 前半部分的指针, 2个指针之间的等于切分元素

步骤:

1.第一个元素为切分元素(v),小于切分元素的指针也在第一个元素上(lt)

1.1 比切分元素大的指针在最后一个元素上gt,

1.2 要进行比较的元素(i),初始时在第2个元素上;

2. 切分元素(v) 和 比较元素(i) 做比较;

2.1 如果i < v ; 那么 小于切分元素的指针lt和i 交换位置; (初始状态的话就是切分元素和第2个元素交换位置,如果不是初始状态,就另外一回事了)

然后lt++; i++;(i++很好理解,比较元素指针移到下一个元素,lt++也不男理解,lt指针前的元素比切分元素小)

2.2 如果i > v ; 那么 大于切分元素的指针gt和i 交换位置;(初始状态的话就是把第2个元素和最后一个元素换位置)

然后gt--;(gt指针后面的元素比v小)//才是进入下一次循环(步骤2),比较元素i的值是之前在最后的那个元素

2.3 如果i = v ; 那么不用交换,(初始时是这样的话,第2个元素和v 相等;)

i++(比较元素的位置加1,就是下次循环时用这个元素了); 

c,b,d,c,b,a    //v=c ;  lt = [0]; gt = [5] ; i = [1];

第一次:i[1]<v;   b,c,d,c,b,a   lt = [1]; gt = [5] ; i = [2]; //[0][1]交换 gt和i交换后lt++ ; i++    

第二次:i[2]>v;   b,c,a,c,b,d   lt = [1]; gt = [4] ; i = [2]; //[2][5]交换 i>v 后,gt和i交换; g--

第三次:i[2]<v;   b,a,c,c,b,d   lt = [2]; gt = [4] ; i = [3]; //[1][2]交换  i<v 后,gt和i交换; lt++ ; i++  

第四次:i[3]=v;   b,a,c,c,b,d     lt = [2]; gt = [4] ; i = [4]; //不交换,     i=v 后,不做交换,比较元素i指针++

第五次:i[4]<v;   b,a,b,c,c,d  //         lt = [3]; gt = [4] ; i = [5]; //[2][4],此时 i++ 后大于gt; 退出循环(步骤2,进入步骤3)

 

   3.此时递归的对小于和大于v的前后2个子数组进行排序

总结:

该三向切分对大量重复的数组有效, 因为其重复的元素多的话, 前后2个子数组会比较短小了

 

2.4 优先队列

与队列(先进先出),栈(后进先出),一样是一种数据类型;它实现了最大(小)的值先出队列的一种数据结构;

就是当前队列中的值是有序的,插入新值,会插入到他的对应位置;比如插入的值是所有当前元素的最大值,那么它会在队列的最前面,如果是做小值,那么会在所有元素的最后面

下面会实现2中最大值的优先队列MaxPQ和MinPQ;按最大值排序和按最小子排序.

定义MaxPQ的API:(MinPQ类似)

构造器3个;默认;制定队列大小, 把一个数组转换为队列

其他方法:

Insert(key v)  向优先队列插入一个元素

max()   返回最大元素

delMax()       删除并返回最大元素

isEmpty()   返回队列是否为空

size()   返回优先队列中的元素个数

 

下面使用MinPQ实现了一个TopM的应用,用来对大量输入的应用统计大量输入排名前M名,如前10名的一个队列;

该TopM也是比较灵活的,因为当遇到大量的输出时,不可能对大量的输入数据做缓存后,再进行排序,再获取前M名的元素, 这在空间和时间上都是浪费,因为只需要M个值(如10个值),确要缓存整个数据和对整个数据排序

TopM的实现

class TopM

{

main(String[] args)

{

int M = Integer.parseInt(args[0]);

MinPQ<Transcation> pq = new MinPQ<Transcation>(M+1);//构造一个指定长度的优先队列(M+1,多预留了一个位置)

while(StdIn.hasNextLine()){ //循环获取输入

pq.insert(new Transcaion(StdIn.readLine()));    //把获取的输入插入队列

if(pq.size()>M ); //判断队列的长度是不是大于M;大于M了就删除其中最小的值,便于下一个输入的值能插入

pd.delMin();

}

Stack<Transaction> stack = new Stack<Transaction>();//构建一个栈, 

while(!pq.isEmpty()){ //从优先队列中输出一个最小的元素,加入栈, 这样从栈中取元素的时候是按从小到大的顺序

stack.push(pq.delMin());

}

for(Transaction t : stack){ //遍历栈, 

StdOut.println(t); //顺序打印,刚输入的元素中前M名的最大元素

}

}

}

2.4.2 初级实现

1. 数组实现(无序)

就是把元素先放入数组中, 等到要删除最大元素时,再对数组进行选择排序的内循环找到最大元素,让把最大元素和边界元素交换,然后删除掉

2. 数组实现(有序)

该方法是在insert()方法中每次插入元素时对数组进行操作,将所有较大的元素向右移动一格以使数组保持有序,然后插入元素(和插入排序一样),

这样,最大的元素总会在数组的一边

总结, 使用链表也能实现上面的这些初级实现, 无序的实现方式属于惰性方法,在删除最大元素时才去查找, 有序的实现属于积极的方法,插入的时候就已经准备好了当前的最大元素, 删除时只有直接取就行了

 他们的效率都非常低,现在要介绍另一种数据结构堆

2.4.3 堆的定义(二叉堆,简称为堆)

数据结构二叉堆(一个父节点可以有2个子节点);在二叉堆的数组中,每个元素都要保证等于等于另外两个特定位置的元素,也就是父(根)节点,必须大于等于它的2个子节点.

定义: 但一颗二叉树的每个结点都大于等于它的两个子结点时, 它被称为 堆有序. 根结点是堆有序的二叉树中最大的结点!

 

二叉堆表示法

如果用指针来表示二叉树的话, 每个结点需要2个指针, 一个指向父结点,2个指向子节点.这样好像满复杂的.如果使用完全二叉树,表达就会变得特别方便,

可以先定下根节点, 然后一层一层地由上向下,从左至右,在每个结点的下方连接2个更小的节点,直到将N个结点全部连接完毕,完全二叉树只需要数组而不需要指针就可以表示.

具体方法是按照层次顺序放入数组中,根结点的位置为[1], 它的子结点的位置[2]和[3],而子结点的子结点则分别在位置[4],[5],[6],[7],以此类推.

定义: 二叉树是一组能够用堆有序的完全二叉树排序的元素, 并在数组中按照层级存储(不使用数组的第一个位置[0],位置技术更方便).

在数组表示的堆中,位置k的结点的父结点的位置为看k/2,而它的两个子结点的位置则分别为2k和2k+1 ,这样不使用指针,我们可以根据这个公式在树中上下移动.找父结点和子结点都很快和方便

2.4.4 堆的算法

上面说了使用数组来构建这个二叉堆, 使用一个长度为N+1 的私有数组pq[]来表示一个大小为N堆,因为数组的[0]下标不使用,为了便于计算,堆的元素在数组的pq[1]~pq[N]之间

在加入新元素或者删除最大(小)元素时,需要对堆进行有序化;

有序化2种情况,如下:

1. 在堆低加入一个新元素时, 需要由下至上恢复堆的顺序,(上浮)

2. 当根结点替换为一个较小的元素(根节点被取走), 需要由下至上恢复推的顺序(下沉)

2种操作下面学习

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

新元素加入到堆,在堆的底部加入新元素后, 新元素可能比它的父结点大,此时堆的有序性被打破,需要进行有序化,

由下至上的堆有序化,可以称为上浮,就是把新元素(K)和父节点(K/2)比较,如果比他的父节点大,那么把父节点和新元素互换位置

上浮的方法取名swim(),代码如下:

private void swim(int k){

while(k > 1 && less(k/2,k)){  //判断:1. k的下标大于根的下标(已经是根也不比去上浮了),2.k/2(父) 小于 K 的话进入循环

exch(k/2,k);  // 子结点比父结点的判断成,交换2个的位置,K成为父节点

k = k/2;  //获取K当前的位置(原来父结点的位置)

}

}

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

当最大(小)元素(根元素)被取走后, 最合适的方式是从堆的末尾(数组末尾)的元素放在根元素原来的位置上,

这样一来,堆顺序被打破, 使用下沉的方式将该元素(临时根元素),和它2个结点([2k], [2k+1]),进行比较如果子结点比他大那么就交换位置,

然后循环这个操作,直到它比它的子结点大,或到达底部位置

下沉的方法为sink(),代码如下:

private void sink(int k){

while(2*k <= N){ // 获取K的子结点,如果有的话,进入循环

int j = 2*k; //获取子节点的下标

if(j < N && less(j,j+1) ) j++;  //判断是不是有2个子节点 和 2个子结点比较谁大,下面用大的子节点和k比较

if(!less(k,j)) break; //如果k 比j 大, 那么结束下沉,k的位置是正确的,

exch(k,j); //否则, k 和 j 交换位置,

k = j;    //获取k当前的下标值;进入下一次循环

}

}

基于堆的优先队列代码如下:

public class MaxPQ<Key extends Comparable<Key>> 

{

private Key[] pq; //基于堆的完全二叉树

private int N = 0;  //存储于pq[1~N] 中, pq[0] 不使用 

//构造器

public MaxPQ(int MaxN){ pq = (Key[])new Comparable[MaxN+1]; }  

 

/** 插入*/

public void insert(Key v){

pq[++N] = v;

swim(N);

}

/**删除最大的元素(根),并返回 */

public Key delMax(){

Key max = pq[1];

exch(1,N--);

pq[N+1] = null; //把数组最后一个位置的引用赋予null

sink(1);

return max;

 

//辅助方法 

public boolean isEmpty(){return N == 0; } 

public int size(){return N; }

private boolean less(int i,int j){ return pq[i].compareTo(pq[j]) < 0;}             //比较方法

private void exch(int i,int j){ Key temp = pq[i]; pq[i] = pq[j]; pq[j] = temp;} //交换方法 

private void swim(int k){..}   //上浮  

private void sink(int k){..}   //下沉  

 

}

总结:

基于堆的完全二叉树表示的优先队列,存储于数组pq[1~N]中,

insert()时N加1,把新元素加入到数组最后,让后用swim() 恢复堆的秩序

delMax()时,从pq[1]中得到需要的返回元素,然后把pq[N]移动到pq[1],N减1,使用sink()恢复堆的秩序.还要把pq中最后一个位置的值赋null

该实现类省略了动态调整数组大小的代码.

 

2.4.4.3 多叉堆

基于完全二叉树构建的堆的思路也可以构建完全三叉树的堆, 位置k的结点的3个子结点在[3k-1],[3k],[3k+1],父结点在[(k+1)/3],

根据这个思路,不管要实现d叉的树都可以实现,要权衡的是增加树高还是增加树叉,那个性能根优而已.其实是一样的性能

 

2.4.4.4 调整数组大小

可以添加一个没有参数的构造函数, 在insert()中添加将数组长度加倍的代码,在delMax()中添加将数组长度减半的代码,(类似前面栈的实现那样).

这样使用这个优先队列的时候, 使用者就不用关注队列大小的限制了

 

2.4.4.5 元素的不可变性

优先队列存储的对象,应该是不可变的,要是一个对象的引用在其他地方的代码中被改变了,那就乱套了!使用优先队列要自觉!