zoukankan      html  css  js  c++  java
  • 剑指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     }
  • 相关阅读:
    JavaScript 资源II
    浮动图片(JS)
    2006/1/14的问题以及解答
    Sql问题
    让SQL飞起来
    当页面上的某个控件回发时,保持滚动条位置的。
    自制的日历控件,可以在日历上DIY事情,相关于记一些随笔的文字之类的话题,如“1/28 是新年啊”
    根据的数据库的level关系绑定数据到控件上.
    十大必备VisualStudio插件
    1、12的经验值
  • 原文地址:https://www.cnblogs.com/boris1221/p/9376788.html
Copyright © 2011-2022 走看看