1、将一个字符串转乘整数
例如“1234”->1234
1 public static void main(String[] args){ 2 String s = "1234"; 3 int cur = 0; 4 int number = 0; 5 while ( cur < s.length() ){ 6 number = number * 10 + s.charAt(cur) - '0'; 7 cur ++; 8 } 9 System.out.println(number); 10 }
现在研究这段代码,有什么问题呢?首先,没有判断正负号,如果输入是“-1234”,就错了;其次,没有考虑int变量的范围,如果超过Integer.maxvalue,程序就会崩溃;最后就是如果输入有特殊字符,比如输入“123#¥%34”,这个时候应该怎么把。哦,还有最重要的一点,如果输入s是空怎么办。
2、求链表倒数第k个节点
思路:两个指针,第一个指针先走k-1步,然后两个指针一起走。
学习前面的思路,这里会有哪些特殊情况呢?首先,链表为空是一种特殊情况;其次,如果链表总长度比k要小,又是一种特殊情况;最后,输入的k为0,又是一种特殊情况。
1 public int lastknode(ListNode head ,int k){ 2 if ( head == null ) return 0; 3 ListNode p = head; 4 ListNode q = head; 5 if ( k > 0 ){ 6 for ( int i = 0 ; i < k - 1 ; i ++ ) { 7 if (p.next == null) return 0; 8 p = p.next; 9 } 10 } 11 while ( p.next != null ){ 12 p = p.next; 13 q = q.next; 14 } 15 return q.val; 16 }
这个代码里面倒数第0个和倒数第1个指向的都是一个节点。。
3、二维数组中查找
题目:在一个二维数组中,每一行都按照从左到右递增的顺序,每一列都按照从上到下递增的顺序排列。给一个整数,要求判断数组中是否存在这个数字。
分析:如果是一维数组,有序情况下,最快的方法就是二分查找。但是这里是二维数组,做法其实和二分类似了,都是不断的缩小查找的范围。
比如这样一个数组:
1 2 8 9 1 2 8 9 1 2 8 9
2 4 9 12 2 4 9 12 2 4 9 12
4 7 10 13 4 7 10 13 4 7 10 13
6 8 11 15 6 8 11 15 6 8 11 15
假如我们要找7,那么如何缩小查找的范围呢。因为左到右,上到下都是递增的。
首先我们考虑如何缩减列,所以第一行8>7,那么8以及8以后的列数就可以忽略了,因为这两列一定都>=8,所以变成第二个数组,灰色部分就是可以忽略的列数部分。这里要注意用一个指针rightedge指向新数组最外面一列。
然后我们考虑如何缩减行,同样的办法,对于rightedge列,从上到下遍历,可以删去两行,如第三个数组所示,浅蓝色部分就是可以忽略的行数部分。这里同样用一个指针downedge来指向新数组的最上面的一行。
这样我们就只需要对左下角这个无色的数组进行遍历寻找就可以了。这里要注意在找列和行的过程中要考虑正好是target值。
1 public boolean findin2D(int[][] nums, int target){ 2 if ( nums[0][0] > target ) return false; 3 int rightedge = 0,downedge = 0; 4 for ( int i = 0 ; i < nums[0].length ; i ++ ){ 5 if ( nums[0][i] == target ) return true; 6 if ( nums[0][i] < target ) rightedge = i ; 7 } 8 for ( int j = 0 ; j < nums.length ; j ++ ){ 9 if ( nums[rightedge][j] == target ) return true; 10 if ( nums[rightedge][j] < target ) downedge = j + 1; 11 } 12 for ( int i = downedge ; i < nums.length ; i ++ ){ 13 for ( int j = 0 ; j <= rightedge ; j ++ ){ 14 if ( nums[i][j] == target ) return true; 15 } 16 } 17 return false; 18 }
4、字符串中字符移动问题
题目:将一个字符串中所有空格替换成"%20",例如输入“we are happy”,输出“we%20are%20happy”。
首先注意到要把空格替换成“%20”,变长了,所以数组肯定要扩容。但是java数组是不允许扩容的,所以只能新建一个长度为newlength的数组。
这个数组长度怎么来呢,newlength = oldlength + count * 2。count就是字符串中空格的个数。
第一个思路:最简单的就是两层循环,从前到后,每一次遇到空格,就把空格以后的所有元素向后移动两位。这样的时间复杂度是O(n^2)。
第二个思路:上面的思路中,对于靠前的一些字符,重复移动了很多次,那么有没有办法一步到位,使时间复杂度降到O(n)呢?考虑使用两个指针,第一个指针oldindex指向s末尾,第二个指针newindex指向新数组的末尾。然后两个指针同时从后向前走,这里就有两种情况:oldindex位置是空格,那么newindex应该插入字符%20;否则,newindex应该插入s.charAt(oldindex)。
整理代码如下:
1 public void movestring(String s){ 2 int count = 0; 3 //计算有多少个需要替换的字符 4 for ( int i = 0 ; i < s.length() ; i ++ ){ 5 if ( s.charAt(i) == ' ' ) count++; 6 } 7 //扩容,newstring数组保存移动后的字符 8 int newlength = s.length() + count * 2; 9 char[] newstring = new char[newlength]; 10 //两个指针,newindex指向新的数组,oldindex指向旧的字符串 11 int newindex = newlength - 1; 12 int oldindex = s.length() - 1; 13 //对新老数组从后往前遍历. 14 while ( oldindex >= 0 ){ 15 if ( s.charAt(oldindex) == ' ' ){ 16 newstring[newindex] = '0'; 17 newstring[newindex-1] = '2'; 18 newstring[newindex-2] = '%'; 19 newindex -= 3; 20 oldindex--; 21 }else { 22 newstring[newindex] = s.charAt(oldindex); 23 newindex--; 24 oldindex--; 25 } 26 } 27 System.out.println(String.valueOf(newstring)); 28 }
这个题目,简单的做法还可以用一个stringbuffer实现,但是这就突出不了算法思想了。
5、链表反向打印
这里思路就很多了。
1 public void helper(ListNode root){ 2 if ( root == null ) return; 3 helper(root.next); 4 System.out.println(root.val); 5 }
这里就可能会问如果链表很长很长,那么递归会有什么问题?堆栈溢出。所以还是用栈做鲁棒性更好。栈做法比较简单,就不写了。
7、两个栈实现一个队列
栈和队列是比较重要的数据结构,用两个栈可以实现一个队列,同样用两个队列也可以实现一个栈。思路都比较雷同了,先来写一下如何用两个栈实现一个队列。
因为队列是先进先出,重写一下push()方法和pop()方法。
思路也比较简单,当调用push的时候,直接将元素压到stack1中,调用pop的时候,先去检查stack2是否为空,若不为空,直接弹出栈顶元素;若为空,则弹出stack1中所有的元素并压入stack2中,然后再弹出一个元素。
1 class Queue <T>{ 2 Stack<T> stack1 = new Stack<>(); 3 Stack<T> stack2 = new Stack<>(); 4 Queue<T> queue = new Queue<>(); 5 public void push(T t){ 6 stack1.push(t); 7 } 8 public void pop() throws Exception { 9 if ( stack2.isEmpty() ) { 10 while (!stack1.isEmpty()) { 11 stack2.push(stack1.pop()); 12 } 13 } 14 if ( stack2.isEmpty() ) throw new Exception("queue is empty!"); 15 stack2.pop(); 16 } 17 }
这里我写了一个泛型类的实现,加上了异常信息。感觉应该比较完整了。
如果是两个队列构成一个栈,过程就比较有趣了。同样是实现push和pop方法:调用push时,找不为空的队列(若两个都为空,随机选取一个),假设是queue1,就压入queue1中,假如queue1中已经压入了a,b,c,d,a是第一个。调用pop时,首先弹出queue1中的元素到queue2中,直到queue1中只剩一个元素,将整个元素弹出即完成删除。(注意两个队列总有一个为空,另一个不为空)然后过程就和上面的类似了。就不详细写代码了。过程参考《剑指offer》62页。
10、二进制中1的个数
问题:输入一个整数,求二进制中,有多少个1。
分析:遇到这种二进制问题,首先就要想到位运算(&、|、^、>>、<<)。
第一个思路:比较n的最后一位是否为1,因为如果最后位是1,那么这个数一定是一个奇数。然后再不停的右移一位,继续比较最后一位,直到为0结束。
1 public int count(int n){ 2 int count = 0; 3 while ( n > 0 ){ 4 if ( n % 2 != 0 ) count++; 5 n = n >> 1; 6 } 7 return count; 8 }
但是这样有个问题,输入负数,可能会陷入死循环。
第二个思路:既然不好移动数字,那可以从右往左一次比较每个位置啊。比如10010,可以设置一个flag=1,第一次比较1和0,第二次比较1和1…,这样就是flag左移,同时用与运算判断n的这个位置是否是1。
1 public int count(int n){ 2 int count = 0; 3 int flag = 1; 4 while ( flag != 0){ 5 if ( (flag & n) != 0) count ++; 6 flag = flag << 1; 7 } 8 return count; 9 }
这个代码可以很好的理解负数,比如-5,在int为32位时,在计算机中存储是:1111 1111 1111 1111 1111 1111 1111 1011(按位取反加1),31个1,代码运行正确。
第三个思路:神技 n & (n-1) 。
一、n-1发生了什么
①、二进制数n,n-1后:如果最后一位是1,那么-1之后变成0;如果最后一位不是1,而从右边开始的第m个位置是1,那么-1之后,第m个位置变成0,第m个位置往右所有位置都变成1(之前都是0)。
所以 二进制 xxxx10000 - 1 = xxxx01111
②、n&n-1
按照上述 n=xxxx10000,n-1=xxxx01111
xxxx10000
xxxx01111
-------------
xxxx0000
可以看到将原来的最右边的1变为0了。
重复操作,有多少个1,这个操作就可以执行多少次。
1 public int count(int n){ 2 int count = 0; 3 while ( n != 0){ 4 count ++; 5 n = n & ( n - 1); 6 } 7 return count; 8 }
12、打印1到最大的n位数
题目:比如给n=5,要求打印1到9999中间的数字。
思路:最基本的方法就不多说了,这里考虑用全排列做。假设n=5,那就是从0000-9999,每个位置都是从0-9的遍历。
这个题非常像求数组的全排列,{a,b,c}的全排列问题,不同的是数组全排列换位之后还要换回去,而这个题不用。写了一个大概的代码思路,和数组全排列类似。全排列的递归解法一定要掌握!!
1 public void helper(int[] array, int cur){ 2 if ( cur == 4 ) { 3 print(array); 4 return; 5 } 6 else { 7 for ( int i = 0 ; i < 10 ; i ++ ){ 8 array[cur+1] = i; 9 helper(array,cur+1); 10 } 11 } 12 }
13、链表删除
链表删除最长常规的就是从头到尾遍历,并且两个指针p、q,p遍历,q指向p的前一个,然后断链删除。时间复杂度是O(n)。
这里介绍一种O(1)的时间复杂度算法。可以这么理解,比如1->2->3->4->5,想要删除3,那么可以把4赋值给3,然后删除4。这样时间复杂度就是O(1)了。当然这里要注意传入的参数一定要是原链表中的某个节点。
1 public ListNode delete(ListNode head, ListNode todelete){ 2 if ( head == null || head == todelete) return null; 3 if ( todelete.next != null ){ //当要删除的节点不是尾节点时 4 ListNode nextnode = todelete.next; 5 todelete.val = nextnode.val; 6 todelete.next = nextnode.next; 7 }else {//当要删除的节点是尾节点时 8 ListNode p = head; 9 while ( p.next != todelete ) p = p.next; 10 p.next = null; 11 } 12 return head; 13 }
感觉一般还是让删除一个链表中值为xxx的多一点,如果是这种情况,就只能遍历来找了。
22、栈的压入、弹出序列
题目:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。如:输入顺序:1、2、3、4、5,则4、5、3、2、1是其输出顺序,但4、3、5、1、2不是其输出顺序
思路:1)如果下一个弹出的数字刚好是栈顶数字,那么直接弹出;
(2)如果一下个弹出的数字不在栈顶,则把压栈序列中还没有入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶;
(3)如果所有数字都压入栈仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列
1 public boolean isseq(int[] array, int[] test){ 2 if ( array.length == 0 || test.length == 0 || array.length != test.length) return false; 3 Stack<Integer> stack = new Stack<>(); 4 int num1 = 0; //指向第一个数组 5 int num2 = 0; //指向第二个数组 6 while ( num2 < test.length ){ 7 while ( num1 < array.length ) { 8 if ( !stack.isEmpty() && stack.peek() == test[num2] ) break; 9 stack.push(array[num1]); 10 num1++; 11 } 12 if ( stack.peek() != test[num2] ) break; 13 stack.pop(); 14 num2++; 15 } 16 return stack.isEmpty(); 17 }