练习
1.4.1 证明从N个数中取三个整数的不同组合为 N(N-1)(N-2)/6. /*下面的证明也恰好符合帕斯卡恒等式(1.1.27)*/
方法一:直接根据排列组合的知识---C(N,3)=N(N-1)(N-2)/6
方法二:数学归纳法如下
1.4.2 修改TreeSum,正确处理两个较大的int值相加可能溢出的情况 /*对一些"极端"情况进行考虑*/
public class ThreeSum { 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((long)a[i]+a[j]+a[k]==0) //采用long来避免溢出情况 cnt++; return cnt; } public static void main(String[] args) { @SuppressWarnings("deprecation") int[] a=In.readInts(args[0]); StdOut.println(count(a)); } }
1.4.3 修改DoublingTest,使用StdDraw产生类似于正文中的标准图像和对数图形,根据需要调整比例使图像总能够充满窗口的大部分区域 /*进一步对库函数StdDraw的理解和运用,以及根据图像分析增长的数量级*/
public class DoublingTest { public static double timeTrial(int N) { int MAX=1000000; int[] a=new int[N]; for(int i=0;i<N;i++) a[i]=StdRandom.uniform(-MAX, MAX); Stopwatch timer=new Stopwatch(); ThreeSum.count(a); return timer.elapsedTime(); } public static void main(String[] args) { int MAX=4000;int i=0; int len=(int)(Math.log(MAX/250)/Math.log(2))+1; int[] arrN=new int[len]; double[] timeN=new double[len]; for(int N=250;N<=MAX;N+=N) { double time=timeTrial(N); StdOut.printf("%7d %5.1f ",N,time); arrN[i]=N; timeN[i++]=time; } // drawStandard(arrN,timeN); drawLog(arrN,timeN); } //绘制标准图形 public static void drawStandard(int[] n,double[] time) { StdDraw.setXscale(-0.5,1.2*n[n.length-1]/1000.0); StdDraw.setYscale(-0.5,time[n.length-1]*1.2); //建立坐标系 StdDraw.setPenColor(StdDraw.BLACK); StdDraw.setPenRadius(0.001); StdDraw.line(0, 0, 1.1*n[n.length-1]/1000, 0); StdDraw.line(0, 0,0, 1.1*time[n.length-1]); for(int i=1;i<1.1*n[n.length-1]/1000.0;i++) StdDraw.text(i,-0.2,i+"K"); for(double t=2;t<time[n.length-1]*1.1;t+=2) StdDraw.text(-0.2, t, t+""); //绘制图像 StdDraw.setPenColor(StdDraw.BOOK_RED); StdDraw.setPenRadius(0.02); for(int i=0;i<n.length;i++) StdDraw.point(n[i]/1000.0, time[i]); } //绘制对数图形 public static void drawLog(int[] arrN,double[] timeN) { //将时间转换为其对数 double[] timelog=new double[timeN.length]; for(int i=0;i<timeN.length;i++) timelog[i]=Math.log(timeN[i]); StdDraw.setXscale(-0.5,1.2*arrN[arrN.length-1]/1000.0); StdDraw.setYscale(-0.5,timelog[arrN.length-1]*1.2); //建立坐标系 StdDraw.setPenColor(StdDraw.BLACK); StdDraw.setPenRadius(0.001); StdDraw.line(0, 0, 1.1*arrN[arrN.length-1]/1000, 0); StdDraw.line(0, 0,0, 1.1*timelog[arrN.length-1]); for(int i=1;i<1.1*arrN[arrN.length-1]/1000.0;i++) StdDraw.text(i,-0.2,i+"K"); for(double t=0;t<timelog[arrN.length-1]*1.1;t+=timelog[arrN.length-1]/5) StdDraw.text(-0.2, t, String.format("%.2f",t)); //绘制图像 StdDraw.setPenColor(StdDraw.BOOK_RED); StdDraw.setPenRadius(0.02); for(int i=0;i<arrN.length;i++) StdDraw.point(arrN[i]/1000.0, timelog[i]); } }
注:以下的实现中并没有根据需要比例自动调整,此外也多了一个MAX来确定范围(上面写的还是有一定问题,待改进)
1.4.5 给出下面这些量的近似 /*主要参考表1.4.2典型的近似 进一步理解近似*/
a. N+1~N
b. 1+1/N~1
c. (1+1/N)(1+2/N)~1
d. 2N3-15N2+N~2N3
e. lg(2N)/lgN~1
f. lg(N2+1)/lgN~2
g. N100/2N~?(模拟了一下,N非常大时NaN)
1.4.6 给出以下代码段的运行时间的增长数量级(作为N的函数): /*粗略了解求解增长数量级的方式*/
a. int sum=0; for(int n=N;n>0;n/=2) for(int i=0;i<n;i++) sum++; b. int sum=0; for(int i=1;i<N;i*=2) for(int j=0;j<i;j++) sum++; c. int sum=0; for(int i=1;i<N;i*=2) for(int j=0;j<N;j++) sum++;
增长数量级取决于 sum++的运行次数
a. (N+N/2+N/4...) 总共有 log2N 个元素,最后一个元素满足 2^(k-1)<=N
b. (1+2+4+...) 最后一个元素满足 2k<=N
c. NlogN
1.4.7 以统计涉及输入数字的算术操作(和比较)的成本模型分析ThreeSum.
即统计 if (a[i] + a[j] + a[k] == 0) 语句执行的次数,由表1.4.4可知 D---N3/6-N2/2+N/3~N3/6
1.4.8 编写一个程序,计算输入文件中相等的整数对的数量.如果你的第一个程序是平方级别的,请继续思考并以Array.sort()给出一个线性对数级别的解答
/*如何将一个N2的算法改进到NlogN是一个很重要也经常需要做的优化*/
public class H1_4_8 { public static void main(String[] args) { //这段是为了产生一些随机数 // int max=100000; // Out out=new Out("h1_4_08.txt"); // for(int i=0;i<300000;i++) // out.printf("%d ", StdRandom.uniform(-max, max)); In in = new In("h1_4_08.txt"); int[] a = in.readAllInts(); in.close(); Stopwatch timer = new Stopwatch(); int cnt = countFast(a); StdOut.println("elapsed time = " + timer.elapsedTime()); StdOut.println(cnt); timer = new Stopwatch(); cnt = count(a); StdOut.println("elapsed time = " + timer.elapsedTime()); StdOut.println(cnt); } //暴力解法 public static int count(int[] arr) { int cnt=0; for(int i=0;i<arr.length;i++) for(int j=i+1;j<arr.length;j++) if(arr[i]==arr[j]) cnt++; return cnt; } //采用排序后的快速计数 public static int countFast(int[] arr) { Arrays.sort(arr); int cnt=0; for(int i=0;i<arr.length;i++) { int j=i; while(j<arr.length-1&&arr[i]==arr[j+1]) { cnt++; j++; } } return cnt; } }
1.4.9 已知由倍率实验可得某个程序的时间倍率为2b且问题规模为N0时程序的运行时间为T,给出一个公式预测该程序在处理规模为N的问题时所需的运行时间. /*对1.4.6节倍率实验巩固*/
首先,由倍率实验时间之间关系为 T(2*N)/T(N)=2b -->从而只需知 N=2kN0 -->k=log2(N/N0) --->T(N)=T*k=T*log2(N/N0)
1.4.10 修改二分查找算法,使之总是返回和被查找的键匹配索引的最小元素(且仍然能够保证对数级别的运行时间) /*二分查找重复元素的一种处理情况*/
public class H1_4_10 { public static int rank(int key, int[] a) { int lo = 0; int hi = a.length - 1; while (lo <= hi) { // Key is in a[lo..hi] or not present. int mid = lo + (hi - lo) / 2; if (key < a[mid]) hi = mid - 1; else if (key > a[mid]) lo = mid + 1; else { while(mid>0&&a[--mid]==key); //该语句排除了左边与之相同的值 return mid+1; } } return -1; } public static void main(String[] args) { int[] arr=new int[]{1,4,1,2,3,3,1,4,5,7,3,5}; Arrays.sort(arr); StdOut.println(Arrays.toString(arr)); StdOut.println(rank(3,arr)); StdOut.println(rank(4,arr)); } }
注:但上述算法的最坏情况并不是logN,例如全部数均相同等极端情况
1.4.11 为StaticSETofInts添加一个实例方法howMany(),找出给定键的出现次数且在最坏情况下所需的运行时间和logN成正比. /*对于数组中重复元素的处理*/
public class StaticSETofInts { private int[] a; public StaticSETofInts(int[] keys) { // defensive copy a = new int[keys.length]; for (int i = 0; i < keys.length; i++) a[i] = keys[i]; // sort the integers Arrays.sort(a); } public boolean contains(int key) { return rank(key) != -1; } public int rank(int key) { int lo = 0; int hi = a.length - 1; while (lo <= hi) { // Key is in a[lo..hi] or not present. int mid = lo + (hi - lo) / 2; if (key < a[mid]) hi = mid - 1; else if (key > a[mid]) lo = mid + 1; else return mid; } return -1; } public int howMany(int key) { int cnt=0; int k=rank(key); //该语句耗费logN int temp=k; while(k>=0&&a[k--]==key) //左边重复情况 cnt++; while(temp>=0&&temp<a.length-1&&a[++temp]==key) //右边重复情况 cnt++; return cnt; } public static void main(String[] args) { int[] arr=new int[]{1,4,5,5,2,1,4,2,3,12,2,12}; StaticSETofInts setints=new StaticSETofInts(arr); StdOut.println(setints.howMany(12)); } }
注:以上情况同样全部元素全相同等特殊情况运行时间非logN---暂时没想到改进方案
1.4.12 编写一个程序,有序打印给定的两个有序数组(含有N个int值)中的所有公共元素,程序在最坏情况下所需的运行时间和logN成正比 /*寻找公共元素,以下方法在合并两个有序数组也采用类似方法*/
public class H1_4_12 { //该函数前提是arrA和arrB均是已排序数组 public static void printTheCommonNum(int[] arrA,int[] arrB) { int ai=0,bi=0; int len=arrA.length+arrB.length; //最坏情况每个元素至少参与一次比较 for(int i=0;i<len;i++) { if(arrA[ai]>arrB[bi]) { bi++; if(bi==arrB.length) //避免数组溢出 break; } else if(arrA[ai]<arrB[bi]) { ai++; if(ai==arrA.length) //避免数组溢出 break; } else { StdOut.printf("%d ", arrA[ai]); ai++;bi++; if(ai==arrA.length||bi==arrB.length) //避免数组溢出 break; } } } public static void main(String[] args) { int[] arr1=new int[]{2,2,3}; int[] arr2=new int[]{1,2,3}; printTheCommonNum(arr1, arr2); } }
只有一个循环,长度为2N,因此为2N正比于N
1.4.13 根据正文中的假设分别给出表示以下数据类型的一个对象所需的内存量: /*以下假设基于对象开销16字节,一般内存的使用都会被填充为8字节(总内存8的倍数)*/
a. Accumulator(1.2.4.3节累加器) 16+8+4=28-->32(剩余4个字节为填充字节)
b. Transaction(1.2.2.2表1.2.6) 16+8+8+8=40
c. FixedCapacityStackOfStrings,其容量为C且含有N个元素(1.3.2.1表1.3.2) 16+40C+4=20+40C-->24+40C
d. Point2D(1.2.2.1表1.2.3) 16+8+8+8+8=48
e. Interval1D(同上) 16+8+8=32
f. Interval2D(同上) 16+8+8+8=40
g. Double 16+8=24
1.4.4 4-sum.为4-sum设计一个算法 /*如何通过2-sum和3-sum进一步思考4-sum的情况---进一步改进可以参考下一题*/
public class H1_4_14 { 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++) for(int k=j+1;k<N;k++) { int temp=BinarySearch.rank(-(a[i]+a[j]+a[k]), a); if(temp==k||temp==j||temp==i) //避免值重复所造成的影响 { temp=k; while(temp<N-1&&a[++temp]==a[k]) cnt++; } else if(temp>k) cnt++; } return cnt; } public static void main(String[] args) { @SuppressWarnings("deprecation") int[] a=In.readInts("input.txt"); StdOut.println(Arrays.toString(a)); StdOut.println(count(a)); } }
以上给出的方式对重复值依旧可行
1.4.15 快速3-sum.作为热身,使用一个线性级别的算法(而非基于二分查找的线性对数级别的算法)实现TwoSumFaster来计算已排序的数组中和为0的整数对的数量.用相同的思想为3-sum问题给出一个平方级别的算法. /*重磅来袭!改进改进再改进~*/
public class H1_4_15 { public static int twoSumFaster(int[] a) { int pos=0,neg; int N=a.length; int cnt=0; for(int i=0;i<N;i++) //找出第一个出现的正数,将数组分为正负两部分 { if(a[i]>=0){ pos=i; break;} } neg=pos-1; for(int i=0;i<N;i++) { if(pos>=N||neg<0) //正负两部分中有一个已达到临界 break; if(a[pos]+a[neg]<0) //正数绝对值小情况 pos++; else if(a[pos]+a[neg]>0) //正数绝对值大情况 neg--; else //相等情况---涉及多组相等,用到排列组合方法 { int samePos=0;int sameNeg=0; int temp=pos; while(pos<N&&a[pos]==a[temp]) { samePos++;pos++;} //其中的pos++不能写到a[pos++]中--因为循环会多加一次 temp=neg; while(neg>=0&&a[neg]==a[temp]) { sameNeg++;neg--;} cnt=cnt+(samePos*sameNeg); } } return cnt; } public static void main(String[] args) { @SuppressWarnings("deprecation") int[] a=In.readInts("input.txt"); Arrays.sort(a); StdOut.println(Arrays.toString(a)); StdOut.println(twoSumFaster(a)); } }
由于上述方法实用性一般(且全0时存在bug),且并不好.根据以下原理进行改进 (其中A,B为同一个数组, i从0开始,j从尾开始)
A(i)+B(j)==K? -->若A(i)+B(j)>k 则 i++, 若A(i)+B(j)<k则 j++ . (基于因为整个序列递增的性质)
public class H1_4_15 { /*A+B=k问题---由于已排序,所以 k-A(1)=B(1)>k-A(2)=B(2) * 因此可以建立first和last分别指向首和尾 * 以下方式不包括重复情况 * */ public static int twoSumFaster(int[] a,int k,int i) { int first=i,last=a.length-1; int cnt=0; while(first<last) { if(a[first]+a[last]<k) first++; else if(a[first]+a[last]>k) last--; else { cnt++; while(first<last&&a[first]==a[++first]); while(last>first&&a[last]==a[--last]); } } return cnt; } /*A+B+C=0 借鉴twoSumFaster. B+C=-A便可 即令k=-A带入 * */ public static int threeSumFaster(int[] a) { int cnt=0; for(int i=0;i<a.length-1;i++) { int k=-a[i]; cnt+=twoSumFaster(a, k, i+1); } return cnt; } public static void main(String[] args) { @SuppressWarnings("deprecation") int[] a=In.readInts("input.txt"); Arrays.sort(a); StdOut.println(Arrays.toString(a)); StdOut.println(twoSumFaster(a,0,0)); StdOut.println(threeSumFaster(a)); } }
1.4.16 最接近的一对(一维).编写一个程序,给定一个含有N个double值的数组a[],在其中找到一对最接近的值: 两者之差(绝对值)最小的两个数.程序在最坏情况下所需的运行时间应该是线性对数级别的.
public class H1_4_16 { //暴力解法 N^2 public static int[] minDiff(double[] a) { int[] index=new int[2]; double mindiff=Double.MAX_VALUE; int N=a.length; for(int i=0;i<N;i++) for(int j=i+1;j<N;j++) if(Math.abs(a[i]-a[j])<mindiff) { mindiff=Math.abs(a[i]-a[j]); index[0]=i;index[1]=j; } return index; } //此处返回差值最小的两个值---先排序,排序耗费NlogN public static double[] minDiffFast(double[] a) { double[] num=new double[2]; double mindiff=Double.MAX_VALUE; double[] temp=new double[a.length]; for(int i=0;i<a.length;i++) temp[i]=a[i]; Arrays.sort(temp); for(int i=0;i<a.length-1;i++) { if(Math.abs(temp[i]-temp[i+1])<mindiff) { mindiff=Math.abs(temp[i]-temp[i+1]); num[0]=temp[i];num[1]=temp[i+1]; } } return num; } public static void main(String[] args) { @SuppressWarnings("deprecation") double[] a=In.readDoubles("inputdouble.txt"); StdOut.println(Arrays.toString(a)); double[] number=minDiffFast(a); int[] index=minDiff(a); StdOut.printf("the min difference two number is %.2f, %.2f ",a[index[0]],a[index[1]]); StdOut.printf("the min difference two number is %.2f, %.2f ",number[0],number[1]); } }
1.4.17 最遥远的一对(一维). 编写一个程序,给定一个含有N个double值的数组a[],在其中找到一对最遥远的值:两者之差(绝对值)最大的两个数.程序在最坏情况下所需的运行时间应该是线性级别的!
???
1.4.18 数组的局部最小元素.编写一个程序,给定一个含义N个不同整数的数组,找到一个局部最小元素:满足a[i]<a[i-1],且a[i]<a[i+1]的索引i.程序在最坏情况下所需的比较次数为 ~2lgN.
1.4.19 矩阵的局部最小元素
1.4.20 双调查找.如果一个数组中的所有元素是先递增后递减的,则称这个数组为双调的.编写一个程序,给定一个含有N个不同int值的双调数组,判断它是否含有给定的整数.程序在最坏情况下所需的比较次数为 ~3lgN /*充分发挥对分查找的优势,以及灵活使用*/
public class H1_4_20 { //查找双调数组的最大值下标 public static int max(int[] a,int lo,int hi) { if(lo==hi) return lo; int mid=lo+(hi-lo)/2; if(a[mid]<a[mid+1]) return max(a,mid+1,hi); else if(a[mid]>a[mid+1]) return max(a,lo,mid); else return mid; } //二分查找---flag=0针对递增情况,flag=1针对递减情况 public static int rank(int[] a,int key,int lo,int hi,int flag) { while(lo<=hi) { int mid=lo+(hi-lo)/2; if(a[mid]<key) { if(flag==0) lo=mid+1; else hi=mid-1; } else if(a[mid]>key) { if(flag==0) hi=mid-1; else lo=mid+1; } else return mid; } return -1; } //查找key是否在数组a中 public static int findKey(int[] a,int key) { int m=max(a,0,a.length); int index1=rank(a,key,0,m,0); int index2=rank(a,key,m,a.length-1,1); if(index1!=-1) return index1; else if(index2!=-1) return index2; else return -1; } public static void main(String[] args) { @SuppressWarnings("deprecation") int[] a=In.readInts("h1_4_20.txt"); StdOut.println(Arrays.toString(a)); int key=9; if(findKey(a,key)>=0) StdOut.printf("the %d is in array ",key); else StdOut.printf("the %d is not in array ",key); } }
1.4.21 无重复值之中的二分查找.用二分查找实现StaticSETofInts(表1.2.15),保证contains()的运行时间为~lgR,其中R为参数数组中不同整数的数量
public class H1_4_21 { private int[] a; public H1_4_21(int[] keys) { a=new int[keys.length]; int[] b = new int[keys.length]; int j=0; for (int i = 0; i < keys.length; i++) a[i] = keys[i]; Arrays.sort(a); for (int i = 0; i < keys.length; i++) { b[j] = a[i]; if(i==0||a[i]!=a[i-1]) j++; } a=new int[j]; for(int i=0;i<j;i++) a[i]=b[i]; } //是否包含key public boolean contains(int key) { return rank(key) != -1; } //二分查找 public int rank(int key) { int lo = 0; int hi = a.length - 1; while (lo <= hi) { int mid = lo + (hi - lo) / 2; if (key < a[mid]) hi = mid - 1; else if (key > a[mid]) lo = mid + 1; else return mid; } return -1; } public static void main(String[] args) { @SuppressWarnings("deprecation") int[] a=In.readInts("h1_4_21.txt"); StdOut.println(Arrays.toString(a)); H1_4_21 s=new H1_4_21(a); StdOut.println(s.contains(2)); } }
注:采用的初始化方式太过繁琐---是否有更好的方式呢?
1.4.22 仅用加减实现的二分查找(Mihai Patrascu). 编写一个程序,给定一个含有N个不同int值的按照升序排列的数组,判断他是否含有给定的整数.只能使用加法或减法以及常数的额外内存空间.程序的运行时间在最坏情况下应该和logN成正比.