剑指offer题目整理

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、链表反向打印

这里思路就很多了。

第一个思路,如果允许改变list结构,可以先将链表翻转,然后顺序打印即可。
第二个思路,不改变list结构,可以用栈做,比较简单的;既然用栈,那肯定也可以递归做。写了一个递归的做法:
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     }
原文地址:https://www.cnblogs.com/boris1221/p/9376788.html