算法与数据结构细节复习笔记(Java实现)
本笔记涉及代码:https://github.com/hackeryang/Algorithms-Fourth-Edition-Exercises
1.java中的length属性是针对数组说的,比如说声明了一个数组,想知道这个数组的长度则用到了length这个属性,例如有个数组int[] a,那么a的长度就是a.length。length()方法是针对字符串String说的,如果想看这个字符串的长度则用到length()这个方法。size()方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看,例如:
List<Object> array=new ArrayList();
array.add(a);
System.out.println(array.size());
2.利用牛顿迭代法求平方根:
public class SqrtNewton {
public static double sqrt(double c){ //c为要开方的数,如2则计算根号2
if(c<0) return Double.NaN;
double err=1e-15; //接近于0的数
double t=c;
while(Math.abs(t-c/t)>err*t) //随着迭代t会变,c/t越来越接近要计算的开平方值t,误差超过err*t(代表小数点15位)时就继续迭代计算,误差小于这个值则代表结果足够接近
t=(c/t+t)/2.0; //详细公式解释链接:https://www.zhihu.com/question/20690553
return t;
}
public static void main(String[] args){
SqrtNewton a=new SqrtNewton();
System.out.println(a.sqrt(2));
}
}
3.颠倒数组元素的顺序:
public class SwapArray {
public static void main(String[] args){
double[] a=new double[10];
int N=a.length;
for(int i=0;i<N/2;i++){
double temp=a[i];
a[i]=a[N-1-i];
a[N-i-1]=temp;
}
}
}
4.随机将double数组中的元素排序:
import edu.princeton.cs.algs4.StdRandom;
public class Shuffle {
public static void shuffle(double[] a){
int N=a.length;
for(int i=0;i<N;i++){
//将a[i]和a[i+N-1]中任意一个元素交换
int r=i+ StdRandom.uniform(N-i);
double temp=a[i];
a[i]=a[r];
a[r]=temp;
}
}
}
5.Double变量初始化为无穷大:使用Double.POSITIVE_INFINITY和Double.NEGATIVE_INFINITY。
6.1/0与1.0/0.0结果不同,前者会产生运行时除以零异常,会终止程序;而后者的结果是Infinity。
7.For循环与等价的while形式的区别:for循环头部的代码与for循环的主体代码在同一个代码段之中,递增变量在一般循环结束之后是不可用的,但是在等价的while循环中,递增变量在循环结束之后依然是可用的。
8.将一个数字转换为二进制:
import edu.princeton.cs.algs4.StdOut;
public class DecimalToBinary {
public static String decimalToBinary(int n){
String resultString="";
for(int i=31;i>=0;i--)
resultString=resultString+(n>>>i&1); //将整数每一个高位右移至相应的最低位再“与”1,结果转换为字符串从左到右排列输出,依然是原来的二进制
return resultString;
}
public static void main(String[] args){
StdOut.println(decimalToBinary(97)); //a的二进制
}
}
9.矩阵转置:
public class ArraySwap {
public static void main(String[] args){
int[][] a={{1,2,3},{4,5,6}};
int[][] temp=new int[a[0].length][a.length]; //三行二列
for(int i=0;i<a[0].length;i++){
for(int j=0;j<a.length;j++){
temp[i][j]=a[j][i];
System.out.print(temp[i][j]+" ");
if(j==a.length-1)
System.out.print("\n");
}
}
}
}
10.柱状图的实现:
public class Histogram {
public static int[] histogram(int[] a,int M){
int[] b=new int[M];
int n=0;
int m=0;
for(int i=0;i<M;i++){
for(int j=0;j<a.length;j++){
if(i==a[j]){
n++;
}
b[i]=n;
}
n=0;
}
for(int i=0;i<M;i++){
m=m+b[i];
}
return b;
}
public static int[] histogram2(int[] a,int M){
int[] h=new int[M];
int N=a.length;
for(int i=0;i<N;i++)
if(a[i]<M)
h[a[i]]++;
return h;
}
public static void main(String[] args){
int a[]={1,2,3,4,5,6,7,8,9,10,11,11,11,12};
int M=13;
int b[]=histogram(a,M);
System.out.println("调用函数后获取的数组:");
for(int i=0;i<b.length;i++){
System.out.println(b[i]);
}
}
}
11.二分查找算法,每递归二分一次,输出就缩进一个空格:
public class Rank {
public static int rank(int key,int[] a){
return rank(key,a,0,16,1);
}
public static int rank(int key,int[] a,int lo,int hi,int deep){
//如果key存在于a[]中,它的索引不会小于lo且不会大于hi
if(lo>hi)
return -1;
int mid=lo+(hi-lo)/2;
for(int i=0;i<deep;i++)
System.out.print(" ");
System.out.println("lo: "+lo+" hi: "+hi);
if(key<a[mid])
return rank(key,a,lo,mid-1,deep+1);
else if(key>a[mid])
return rank(key,a,mid+1,hi,deep+1);
else
return mid;
}
public static void main(String[] args){
int[] array={0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
rank(10,array);
}
}
12.二项分布发生k次的概率:
public class BinomialSample {
/*
* 使用一个二维数组来存储各项二项分布的概率
* 行代表重复N次实验,列代表发生k次,所以在下面循环条件中需要j<=i
* */
public static double[][] binomial(int N,int k,double p){
double[][] array=new double[N+1][k+1];
//给二维数组初始化第一列,避免下面执行时出现数组下标越界
array[0][0]=1.0;
for(int i=1;i<N+1;i++)
array[i][0]=array[i-1][0]*(1-p);
for(int i=1;i<N+1;i++)
for(int j=1;j<=i && j<k+1;j++)
array[i][j]=(1-p)*array[i-1][j]+p*array[i-1][j-1]; //前i-1次实验中已经发生j-1次的概率加上前i-1次实验中没有已经发生j次的概率
return array;
}
public static void main(String[] args){
double[][] array=binomial(100,50,0.25); //100次实验中发生50次的概率,每一次实验中发生的概率为0.25
System.out.println(array[100][50]);
}
}
13.a为数组,a.toString()与Array.toString(a)的区别:Object 类的 toString 方法返回一个字符串,该字符串由类名(对象是该类的一个实例)加上“@”和此对象哈希码的无符号十六进制表示组成。a.toString(),a虽然是数组名,但在这是一个对象实例,而Arrays.toString()方法是返回指定数组内容的字符串表示形式。例如:
boolean[] a1 = new boolean[size];
Arrays.fill(a1, true);
System.out.println("a1 = " + Arrays.toString(a1));
Arrays.fill(a1, true);
System.out.println("a1 = " + a1.toString());
输出为:a1 = [true, true, true, true, true, true]
a1 = [aaa@qq.com]
14.int的取值范围是-231~231-1之间的整数。抽象数据类型与静态方法的主要不同点在于将数据和函数的实现关联,并将数据的表示方式隐藏起来。在使用抽象数据类型时,我们的精力集中在API与方法的操作上而不会关心数据的表示;在实现抽象数据类型时,我们的精力集中在数据本身并实现对数据的各种操作。
15.对象是能够承载数据类型的值的实体。对象的三大重要特性:状态,标识和行为。对象的状态即数据类型中的值。对象的标识可以认为是在内存中的位置。对象的行为就是对数据类型的操作。引用是访问对象的方式(如声明Counter heads=new Counter(),这个heads就是引用),引用所在地址中的内容是对象实例(依然是heads)具体数据的内存地址。相当于找到名字的地址只是知道具体信息在哪,要知道它的具体信息还要到名字地址中指向的那个地址去访问。
16.创建对象中的new被触发之后,会有如下底层过程:
(1)为新的对象实例分配内存空间;
(2)调用构造函数初始化对象中的值;
(3)返回该对象实例的一个引用。
17.静态与非静态方法的区分:静态方法调用的开头(.的前面)是类名,而非静态方法的调用者总是对象实例名。静态方法的主要作用是实现函数,而非静态方法的作用是实现实例数据的操作。
18.对于原始数据类型例如int,则x=y会把y的值复制到x中,但是如果是引用变量,会使x和y指向同一个内存地址,修改x或y其中一个的状态,另一个也会改变,因为修改的不是x和y本身,它们只是指向一个共有内存地址,真正改变的是那个共同指向的内存地址中的数据,例如:
Counter c1=new Counter(“ones”);
c1.increment();
Counter c2=c1;
c2.increment();
StdOut.println(c1); //会返回2 ones
19.Java中的方法只能有一个返回值,但是如果返回对象,就可以实际上返回多个值,例如:
public static Counter max(Counter x,Counter y){
if(x.tally()>y.tally()) return x;
else return y;
}
20.在Java中,所有非原始数据类型的值都是对象,所以数组也是对象。与其他对象相同,将数组传递给一个方法或者将一个数组变量放在赋值语句的右侧时,都是在创建该数组的一个引用,而不是数组的副本。
21.对象数组是一个由对象的引用组成的数组,而非所有对象本身的全部数据的数组。如果对象很大,那么在移动它们时只需操作引用而不是对象本身,可以大大提升效率,但是如果对象很小,每次获取具体数据还需要经过引用反而会降低性能。
22.运用数据抽象的思想编写代码,即定义和使用数据类型,将数据类型的值封装对象中的方式称为面向对象编程。
23.\\s+表示一个或多个制表符、空格、换行符或回车。
24.参数变量的作用域是整个方法,局部变量的作用域是当前代码段中它的定义之后的所有语句。而实例变量为该类的对象保存了数据类型的值,作用域是整个类,如果与实例变量重名,可以使用this前缀来区别实例变量,例如:
Public class Example{
Private int var;(实例变量)
…
Private void method1()
{
int var;(局部变量)
… var …(局部变量)
… this.var …(实例变量)
}
Private void method2()
{
… var … (实例变量)
}
}
25.Java不允许将函数作为参数。对于字符串重新赋值是指向了新的对象而不是改变原有的值,例如:
String string1=”hello”;
String string2=string1;
String1=”world”;
StdOut.println(string1);
StdOut.println(String2);
结果为: world
Hello
同时,String的所有字符串方法都会返回一个新的String对象而不是改变原对象的值,例如s.toUpperCase()不会改变s的值,除非使用s=s.toUpperCase()。
26.在Java中,数组名称是数组的引用(类似于指针),如下代码:
int[] t=a; a=b; b=t;
只是交换两个数组的引用。交换引用的效率与数组大小无关,只占用常数时间。
27.在 Java 中,双斜杠\\ 表示:我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义,如\\s+表示匹配一至多个空格。
28.求均值和标准差:
import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
public class Stats {
public static void main(String[] args){
Bag<Double> numbers=new Bag<Double>();
while(!StdIn.isEmpty())
numbers.add(StdIn.readDouble());
int N=numbers.size();
double sum=0.0;
for(double x:numbers)
sum+=x;
double mean=sum/N;
sum=0.0;
for(double x:numbers)
sum+=(x-mean)*(x-mean);
double std=Math.sqrt(sum/N-1); //不是N,这是一个数理统计中的推导结论,取N-1可以更接近某种特定分布,取的是样本均值
StdOut.printf("Mean:%.2f\n",mean);
StdOut.printf("Stddev:%.2f\n",std);
}
}
29.由于历史和技术原因,Java不允许创建泛型数组,可以把Item[]a=new Item[cap];语句利用类型转换改为:
Item[] a=(Item[]) new Object[cap];
但是数组一旦创建,其大小是无法改变的,因此选择用数组表示栈内容意味着实例必须预先估计栈的最大容量,在push()方法需要在代码中检测栈是否已满。
30.下压栈的实现(后进先出):
import java.util.Iterator;
public class ResizingArrayStack<Item> implements Iterable<Item> {
private Item[] a=(Item[]) new Object[1]; //栈元素
private int N=0; //元素数量
public boolean isEmpty(){return N==0;}
public int size(){return N;}
private void resize(int max){
//将栈移动到一个大小为max的新数组
Item[] temp=(Item[]) new Object[max];
for(int i=0;i<N;i++)
temp[i]=a[i];
a=temp;
}
public void push(Item item){
//将元素添加到栈顶
if(N==a.length) resize(2*a.length);
a[N++]=item;
}
public Item pop(){
//从栈顶删除元素
Item item=a[--N];
a[N]=null; //避免对象游离
if(N>0 && N==a.length/4) resize(a.length/2);
return item;
}
public Iterator<Item> iterator(){
return new ReverseArrayIterator();
}
private class ReverseArrayIterator implements Iterator<Item>{
//支持后进先出的迭代
private int i=N;
public boolean hasNext(){return i>0;}
public Item next(){return a[--i];}
public void remove(){ }
}
}
31.链表进行循环遍历时效率不高,但插入和删除性能优势明显。
32.数组是在运行时才去判断数组元素的类型约束,而泛型正好相反,在运行时,泛型的类型信息是会被擦除的,只有编译的时候才会对类型进行强化。所以当传入参数的类型与初始化类型声明时的类型不匹配时,数组只会在运行的时候报错,而泛型在编译的时候就能发现错误不通过。也就是说,在使用泛型时,Java会在编译时检查类型的安全性,但会在运行时抛弃所有这些信息。详细链接:https://blog.csdn.net/xsc_c/article/details/18010499
33.编译成.class文件后,Java的命名规则会使用$分隔外部类和内部类,例如Stack.class和Stack$Node.class,其中Node为Stack的内部类。
34.迭代器Iterator类必须实现两个方法,hasNext()和next()。将一个链表栈中的项按照原顺序复制到另一个链表栈中,需要两次迭代器遍历以及两次入栈操作来将顺序负负得正,例如:
import java.util.Iterator;
public class Stack2<Item> implements Iterable<Item> {
/*
* 链表实现
* */
private int N;
private Node first;
private class Node{
Item item;
Node next;
}
public Stack2(){
N=0;
first=null;
}
public void push(Item item){
Node oldFirst=first;
first=new Node();
first.item=item;
first.next=oldFirst;
N++;
}
public Item top(){
return first.item;
}
public Item pop(){
Item item=first.item;
first=first.next;
N--;
return item;
}
public boolean isEmpty(){
//或first=null
return N==0;
}
public int size(){
return N;
}
public Iterator<Item> iterator(){
return new ArrayIterator();
}
private class ArrayIterator implements Iterator<Item>{
private Node current=first;
public boolean hasNext(){
return current!=null;
}
public Item next(){
Item item=current.item;
current=current.next;
return item;
}
public void remove(){ }
}
public static Stack2<String> copy(Stack2<String> stack2){
//链表栈是在栈的最上层添加链表项,最先添加的项会在栈底,用迭代器遍历是从栈顶顺序遍历,再push入栈中会被逆序,所以需要第二次迭代器遍历再push入栈来第二次逆序,逆逆得正
Stack2<String> resultStack=new Stack2<String>();
//逆序1
Stack2<String> tempStack=new Stack2<String>();
Iterator<String> iterator=stack2.iterator();
while(iterator.hasNext()){
tempStack.push(iterator.next());
}
//逆序2
Iterator<String> tempIterator=tempStack.iterator();
while(tempIterator.hasNext()){
resultStack.push(tempIterator.next());
}
return resultStack;
}
public static void main(String[] args){
Stack2<String> stack=new Stack2<String>();
stack.push("我");
stack.push("的");
stack.push("名字");
stack.push("叫");
stack.push("yyc");
stack.push("hacker");
//打印
System.out.println("原栈逆序输出:");
for(String string:stack){
System.out.print(string);
}
System.out.println("");
System.out.println("开始拷贝。。。");
//拷贝
Stack2<String> stack2=Stack2.copy(stack);
System.out.println("开始打印拷贝后的栈");
//创建迭代器
for(String string:stack2){
System.out.print(string);
}
}
}
35.删除链表的第k个元素:
public class LinkedListExercise2<Item> {
private static class Node<Item>{
Node next;
Item item;
}
/*
* @param k 第k个元素
* @param first 链表的首节点
* @return 新的链表
* @throws Exception
* */
public Node<Item> delete(int k,Node<Item> first) throws Exception{
if(k<0 || first == null) return null;
if(k==0){
first=first.next;
return first;
}
Node<Item> current=first;
while(current!=null && --k!=0){
current=current.next;
}
if(k!=0 || current.next==null || current==null){
throw new Exception();
}else{
current.next=current.next.next; //将当前第k-1个链表元素的下一个指向链接(即next)直接跳过下一个(即k),指向下下个(即k+1)
}
return first;
}
public static void main(String[] args){
/*
* 创建链表
* */
Node<String> first=new Node<String>();
Node<String> second=new Node<String>();
Node<String> third=new Node<String>();
Node<String> forth=new Node<String>();
Node<String> fifth=new Node<String>();
first.item="我的";
first.next=second;
second.item="名字";
second.next=third;
third.item="叫做";
third.next=forth;
forth.item="yyc";
forth.next=fifth;
fifth.item="hacker";
fifth.next=null;
//遍历原链表
System.out.println("原链表:\n------");
Node current1=first;
while(current1.next!=null){
System.out.println(current1.item);
current1=current1.next;
}
System.out.println(current1.item); //前一个遍历只输出到倒数第二个节点,输出最后一个
System.out.println("------");
LinkedListExercise2<String> linkedListExercise2=new LinkedListExercise2<>();
//删除第4个元素
int k=4;
System.out.println("正在删除第"+k+"个节点...");
Node<String> resultNode=null;
try{
resultNode=linkedListExercise2.delete(k,first);
}catch(Exception e){
e.printStackTrace();
}
System.out.println("删除成功");
System.out.println("新链表:\n------");
Node current2=resultNode;
}
}
36.在某个链表项后面插入一个新链表项:
public Node<Item> insertAfter(Node<Item> targetNode,Node<Item> node,Node<Item> first){
if(targetNode==null || node==null){
return first;
}
Node<Item> current=first;
while(current!=null){
if(current.equals(targetNode)){
Node<Item> t=current.next; //在插入前,targetNode原本下一个节点为t
current.next=node; //将节点插入到targetNode的后面
node.next=t; //确保插入的节点既向前链接到targetNode,又要向后链接到原来targetNode下一个节点的的节点t
return first;
}
current=current.next; //向后迭代
}
return null;
}
37.删除链表中所有item值为key的链表项,在remove()方法中接受一条链表和字符串key作为参数:
public class LinkedListExercise6 {
private static class Node{
Node next;
String item;
@Override
public String toString() {
return "item: "+item;
}
}
public Node remove(Node first,String key){
Node newFirst=new Node();
newFirst.next=first;
Node current=newFirst;
while(current.next!=null){
if(current.next.item.equals(key)){
current.next=current.next.next; //跳过要删除的节点,直接链接到下下个节点
}else{
current=current.next;
}
}
return newFirst.next;
}
/*
* @param args
* */
public static void main(String[] args){
/*
* 创建链表
* */
Node first=new Node();
Node second=new Node();
Node third=new Node();
Node forth=new Node();
Node fifth=new Node();
first.item="我的";
first.next=second;
second.item="名字";
second.next=third;
third.item="叫";
third.next=forth;
forth.item="yyc";
forth.next=fifth;
fifth.item="hacker";
fifth.next=null;
LinkedListExercise6 linkedListExercise6=new LinkedListExercise6();
Node resultNode=linkedListExercise6.remove(first,"我的");
System.out.println("新链表:\n------");
Node current2=resultNode;
while(current2.next!=null){
System.out.println(current2.item);
current2=current2.next;
}
System.out.println(current2.item); //因为前面while循环只会循环到链表倒数第二项,最后一项的next为null所以不会被println出来,所以在这里要再println出链表最后一项
System.out.println("------");
}
}
38.约瑟夫环问题:
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdOut;
public class Josephus {
public static void main(String[] args){
int m=3;
int N=41;
Queue<Integer> queue=new Queue<Integer>();
for(int i=0;i<N;i++)
queue.enqueue(i);
while(!queue.isEmpty()){
for(int i=0;i<m-1;i++)
queue.enqueue(queue.dequeue()); //每次都把队列从队列头上踢出m-1项,再把踢出的项加回到队列尾部,这样第m项就会出现在队列头上
StdOut.print(queue.dequeue()+" "); //踢掉队列头上的第一项,就是上一个步骤被排出来的第m项,然后在while大循环中继续这样的排列踢出流程,直到所有项都被踢出去
}
StdOut.println();
}
//上面的采用队列的方法占用内存较大且算法复杂度较大,下面这种递归方法效率更好
public int lastRemaining(int n, int m) {
if(n<=0) return -1;
if(m<=0) return n-1;
if(n==1) return 0;
/*
* 采用递归思想,与通过哈希值来编号分区一样,当有n个人时,在第一轮中报数为m-1的人编号为(m-1)%n。设最后留下的人编号结果为f[n];
* 同理已经求得n-1个人情况下最后剩下的人的编号为f[n-1],则f[n]=(f[n-1]+m)%n,
* 因为上一轮出列一个人后,下一个人重新从0开始编号,所以n人为第一轮,n-1人为第二轮的情况下,第二轮把每个人原来的编号都减了m,
* 因此重新加上m就能恢复到上一轮原来的编号,如此递归下去,只剩下一个人的情况即f[1]=0
* */
return (lastRemaining(n-1,m)+m)%n;
}
}
39.在内存使用上,64位的操作系统,一个内存地址需要8字节(也叫机器字),而32位操作系统的内存地址为4字节。一个对象所使用的内存量,需要将所有实例变量所使用的内存与对象本身的开销(一般为16字节)相加。开销包括一个指向对象的类的引用、垃圾收集信息以及同步信息(填充字节主要是为了使对象所占内存凑满8的倍数)。例如,一个Integer对象使用24字节(16字节的对象开销,4字节用于保存int值以及4字节的填充字节)。同样,一个Date对象需要32个字节:16字节的对象开销,3个int实例变量共需12字节,以及4个填充字节。而Counter对象需要使用32个字节:16字节的对象开销、8字节用于String型实例变量(即一个引用占8字节)、4字节用于int实例变量,以及4个填充字节。链表中的Node对象需要40字节:16字节的对象开销、指向Item和Node对象的引用总共需要16字节、以及8字节的额外开销(这里的填充字节就与前面的4字节不同,都是为了使所占内存凑满8的倍数)。
40.一个含有N个整数的基于链表的栈需要使用(32+64N)字节,包括Stack对象的16字节的开销,引用类型实例变量8字节,int型实例变量4字节,4个填充字节,每个元素需要64字节,即一个Node对象的40字节和一个Integer对象的24字节。
41.Java中数组被实现为对象,一个原始数据类型的数组一般需要24字节的头信息(16字节的对象开销,4字节用于保存长度以及4填充字节)加上保存值所需的内存。一个含有N个int值的数组需要使用(24+4N)字节(会被填充为8的倍数),而一个含有N个double值的数组需要使用(24+8N)字节。
42.一个对象的数组就是一个对象的引用的数组,因此内存占用除对象所需内存外还需加上引用所需内存。例如,一个含有N个Date对象的数组需要24字节(数组开销)加上8N字节(所有引用)加上每个对象的32字节,总共(24+40N)字节。
43.二维数组是一个数组的数组(每个数组都是一个对象)。例如,一个M*N的double类型的二维数组需要使用24字节(数组的数组的开销)加上8M字节(所有元素数组的引用),加上24M字节(所有元素数组头信息的开销)加上8MN字节(M个长度为N的double类型的数组),总共(8MN+32M+24)~8MN字节。
44.String的标准实现有4个实例变量:一个指向字符数组的引用(8字节)和三个int值(12字节)。第一个int值表示字符数组中的偏移量,第二个int值是一个计数器(字符串长度),第三个int值是一个散列值,在某些情况下可以节省一些计算。每个String对象会使用40字节(16字节表示对象,三个int实例变量各需4字节,加上数组引用的8字节和4个填充字节)。这是除字符数组以外字符串所需的内存空间,字符所需内存还需另外计算。
45.一个长度为N的String对象需要(64+2N)字节(40字节的String对象本身,加上字符数组的24+2N字节)。当调用substring()方法时,会创建一个新的String对象(40字节),但它仍然重用了与原来String对象相同的字符数组value[],因此原字符串的子字符串只会增加40字节的内存。因此,一个子字符串所需的额外内存是一个常数,构造一个子字符串所需的时间也是常数。substring(beginIndex, endIndex)方法的取值区间为左闭右开[ )。
46.当Java程序调用一个方法时,系统会从内存中的一个特定区域为方法分配所需内存,用来保存局部变量,这个区域叫做栈(Java系统的下压栈)。当方法返回时,所占用内存也被返回给系统栈。因此,在递归程序中创建数组或其他大型对象很危险,因为每一次递归调用都会使用大量内存。
47.当通过new创建对象时,系统会从堆内存的另一块特定区域为该对象分配所需内存。所有对象都会一直存在,直到对其的引用消失为止。此时系统的垃圾回收进程会将它占用的内存回收到堆中。而指向该对象的引用存放在栈中,引用变量在程序运行到其作用域之外时被释放。
48.堆的优势是可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些不再使用的数据。但缺点是,由于要在运行时动态分配内存,存取速度较慢。栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。
49.在命令行模式下要输入数据至程序中时,可以使用标准输入串对象System.in。但是,我们并不经常直接使用它,因为System.in提供的 read方法每次只能读取一个字节的数据,而我们平时所应用的通常是读取一个字符串或者是一个数字,所以read方法所以提供的功能并没有太大的用处。获取键盘输入有两种方法:
(1)利用Scanner类。例如:
import java.util.Scanner;
public class TestScanner{
public static void main(String[] args){
Scanner scan = new Scanner(System.in);
System.out.println("请输入一个字符串:");
System.out.println("您输入的字符串是:" + scan.next());
}
}
在创建Scanner类的对象时,需要用System.in 作为它的参数,也可以将Scanner看作是System.in对象的支持者,System.in取得用户输入的内容后,交给Scanner来作一些处理。
Scanner类中提供了多个方法:
next():取得一个字符串;
nextInt():将取得的字符串转换成int类型的整数;
nextFloat():将取得的字符串转换成float型;
nextBoolean():将取得的字符串转换成boolean型;
但是Scanner取得输入的依据是空格符,包括空格键,Tab键和Enter键。当按下其中的任一键时,Scanner就会返回下一个输入。当输入的内容中间包括空格时,使用Scanner就不能完整的获得输入的字符串。
(2)使用BufferedReader。例如:
import java.io.BufferedReader;
public class TestBufferedReader{
public static void main(String[] args) throws IOException{
BufferedReader buffer = new BufferedReader(new InputStreamReader(System.in));
System.out.println("请输入一串字符串");
String text = buffer.readLine();
System.out.println("您输入的字符串是:" + text);
buffer.close();
}
}
readLine()方法会返回用户在按下Enter键之前的所有字符输入。使用BufferedReader对象的readLine()方法必须处理java.io.IOException异常(Exception)。
50.常用算法的复杂度与增长数量级:
51.一棵树的大小是它的节点的数量。树中的一个节点的深度是它到根节点的路径上的链接数。树的高度是它所有节点中的最大深度。
52.仅用加减实现的二分查找,只能使用加法和减法以及常数的额外内存空间。程序的运行时间在最坏情况下与logN成正比:
import java.util.Arrays;
public class FibonacciSearch {
private final int FI_SIZE=20;
public int fbSearch(int[] array,int target) {
if (array == null || array.length == 0) {
return -1;
} else {
int length = array.length;
//制造一个长度为20的斐波那契数列
int[] fb = makeFbArray(FI_SIZE);
int k = 0;
//找出数组的长度在斐波数列(减1)中的位置,将决定如何拆分,即F(n-2)+F(n-1)=F(n)中,F(n-1)<length<F(n),
//这样就可以把数组array根据length拆分成前F(n-2)个元素与后F(n-1)个元素两部分,看目标数在前后哪一个部分里面,
//然后递归拆分,模拟二分查找
while (length > fb[k] - 1) {
k++;
}
//构造一个长度为fb[k]-1的新数列
int[] temp = Arrays.copyOf(array, fb[k] - 1);
for (int i = length; i < temp.length; i++) {
if (i >= length) {
temp[i] = array[length - 1]; //用array数组的最后一个元素将temp数组大于length部分的位置填充满,length大小之前的元素都来自array
}
}
int low = 0;
int high = array.length - 1;
while (low <= high) {
int middle = low + fb[k - 1] - 1;
if (temp[middle] > target) {
high = middle - 1;
k = k - 1; //如果目标元素在后面那一段,也就是F(n-1)那一段,原本长度是F(n),k减1就变成 F(n-1)那一段,在此基础上再进行斐波那契数列拆分,变成F(n-2)和F(n-3)两部分
} else if (temp[middle] < target) {
low = middle + 1;
k = k - 2; //如果目标元素在前面那一段,也就是F(n-2)那一段,原本长度是F(n),k减2变成F(n-2)那一段,在此基础上在进行斐波那契数列拆分,变成F(n-3)和F(n-4)两部分
} else {
if (middle <= high) {
//若相等则说明mid即为查找到的位置
return middle;
} else {
//middle的值已经大于high,进入扩展数组的填充部分,即最后一个数就是要找的数
return high;
}
}
}
return -1;
}
}
public static int[] makeFbArray(int length){
int[] array=null;
if(length>2){
array=new int[length];
array[0]=1;
array[1]=1;
for(int i=2;i<length;i++){
array[i]=array[i-1]+array[i-2];
}
}
return array;
}
public static void main(String[] args){
int[] array={1,5,15,22,25,31,39,42,47,49,59,68,88,88,88,88,88};
FibonacciSearch search=new FibonacciSearch();
System.out.println("result: "+search.fbSearch(array,31));
}
}
上一篇: python学习第七天
推荐阅读
-
详解java数据结构与算法之双链表设计与实现
-
数据结构与算法(六)迷宫回溯算法(Java实现)
-
死磕数据结构与算法——哈希表(java实现)。才疏学浅,如有错误,及时指正
-
Java数据结构与算法:图、图的概念、深度优先搜索DFS、广度优先搜索BFS、思路分析、代码实现
-
数据结构与算法(Java描述)-15、稀疏矩阵以及稀疏矩阵的三元组实现
-
Java数据结构与算法之队列(Queue)实现
-
Java 数据结构与算法:递归实现八皇后问题、思路分析、代码实现
-
Java数据结构与算法:单向环形链表、约瑟夫问题、思路分析、代码实现
-
Java-数据结构与算法--数组模拟环形队列实现
-
java数据结构与算法之插入算法实现数值排序示例