劍指offer中幾道演算法題的思考

豌豆射手_BiuBiu發表於2018-09-02

1、在一個二維陣列中,每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函式,輸入這樣的一個二維陣列和一個整數,判斷陣列中是否含有該整數。

  • 假如二維陣列為 {{1, 2, 8, 9}, {2, 4, 9, 12}, {4, 7, 10, 13}, {6, 8, 11, 15}};同時查詢的值為15

    • 常規的思路為我們取到每個二維陣列的值,然後遍歷每個值,判斷是否相等!同時我們知道會迴圈16次。程式碼如下
        private boolean lookUpInTwoDimensionalArrays(int[][] a, int num) {
        // 至少保證 長度至少為1,且還有元素,
        if (a == null || a.length < 1 || a[0].length < 1) {
            return false;
        }
        int b = 0;
        for (int i = 0; i < a.length; i++) {
            for (int j = 0; j < a[i].length; j++) {
                b++;
                if (num == a[i][j]) {
                    // 一共迴圈多少次:12
                    System.out.println(TAG + "一共迴圈多少次:" + b);
                    return true;
                }
            }
        }
        return false;
       }
      複製程式碼
    • 解題的思路如下
      • 1、選取二維陣列的中的第一個元素,比較最右邊的值,如果相等,就查詢結束
      • 2、選取二維陣列的中的第一個元素,比較最右邊的值,如果大於,查詢的就是這個元素,同時角標減去1,然後繼續查詢
      • 3、選取二維陣列的中的第一個元素,比較最右邊的值,如果大於,查詢的不是這個元素,就去查詢下一個陣列,然後繼續查詢
        二維陣列的查詢過程.png
  • 最終的程式碼如下

 /**
     * 陣列是遞增的 ,從左到右  從上到下,那麼陣列一定是個正方形的樣子
     *
     * @param arr 原始的陣列
     * @param num 需要查詢的數字
     * @return 是否找到了
     */
    private boolean newLookUpInTwoDimensionalArrays(int[][] arr, int num) {
        if (arr == null || arr.length < 1 || arr[0].length < 1) {
            return false;
        }
        int rowTotal = arr.length;// 陣列的行數
        int colTolal = arr[0].length;// 陣列的列數
        //開始的角標
        int row = 0;
        int col = colTolal - 1;
        int i = 0;
        while (row >= 0 && row < rowTotal && col >= 0 && col < colTolal) {
            // 是二維陣列的 arr[0][arr[0].length-1] 的值,就是最右邊的值
            i++;
            if (arr[row][col] == num) {
                System.out.println(TAG + "newLookUpInTwoDimensionalArrays 執行了多少次" + i);
                return true;
            } else if (arr[row][col] > num) {// 如果找到的值 比目標的值大的話,就把查詢的列數減去1
                col--;//列數減去1 ,代表向左移動
            } else {// 比目標的num大的,就把行數加上1,然後往下移動
                row++;
            }
        }
        return false;
    }
複製程式碼
  • 如果查詢的值為15,那麼舊的程式碼會執行15次,新的程式碼只會執行3次。

2、請實現一個函式,把字串中的每個空格替換成"%20",例如“We are happy.”,則輸出“We%20are%20happy.”.

  • 第一種方法:先判斷字串中空格的數量。根據數量判斷該字串有沒有足夠的空間替換成"%20"。如果有足夠空間,計算出需要的空間。根據最終需要的總空間,維護一個指標在最後。從後到前,遇到非空的就把該值挪到指標指向的位置,然後指標向前一位,遇到“ ”,則指標前移,依次替換為“02%”。
  public char[]  replaceBlank(char[] string, int usedLength) {
        // 判斷輸入是否合法
        System.out.println(TAG+"string.length="+string.length);
        System.out.println(TAG+"usedLength="+usedLength);
        if (string == null || string.length < usedLength) {
            return null;
        }

        // 統計字元陣列中的空白字元數
        int whiteCount = 0;
        for (int i = 0; i < usedLength; i++) {
            if (string[i] == ' ') {
                whiteCount++;
            }
        }
        // 如果沒有空白字元就不用處理
        if (whiteCount == 0) {
            return string;
        }
        // 計算轉換後的字元長度是多少
        int targetLength = whiteCount * 2 + usedLength;
         //新的儲存的字串的陣列
        char[] newChars = new char[targetLength];
        int tmp = targetLength; // 儲存長度結果用於返回
//        if (targetLength > string.length) { // 如果轉換後的長度大於陣列的最大長度,直接返回失敗
//            return -1;
//        }
        // todo  必須先做 這個,注意體會  --i  和 i-- 的區別
        usedLength--; // 從後向前,第一個開始處理的字元
        targetLength--; // 處理後的字元放置的位置
        // 字元中有空白字元,一直處理到所有的空白字元處理完
        while (usedLength >= 0 && usedLength < targetLength) {
            // 如是當前字元是空白字元,進行"%20"替換
            if (string[usedLength--] == ' ') {
                newChars[targetLength--] = '0';
                newChars[targetLength--] = '2';
                newChars[targetLength--] = '%';
            } else { // 否則移動字元
                newChars[targetLength--] = string[usedLength];
            }
            usedLength--;
        }
        return newChars;
    }
複製程式碼
  • 第二種方法,非常的消耗時間和記憶體。
 private String idoWorkReplace(String str, String tagStr) {
        if (str == null || str.length() < 1) {
            return "";
        }
        String[] split = str.split(" ");
        StringBuffer stringBuffer = new StringBuffer();
        for (String s : split) {
            stringBuffer.append(s);
            stringBuffer.append(tagStr);
        }
        CharSequence charSequence = stringBuffer.subSequence(0, stringBuffer.length() - tagStr.length());
        return charSequence.toString();
    }
複製程式碼
  • 第三種方法,呼叫replace方法,如果僅僅來講替換字串的話,是最好的方法,關鍵的方法是indexOf(this, targetStr, lastMatch).

 StringBuffer sb = new StringBuffer();
        sb.append(str);
        // todo  最節能的方法
        sb.toString().replace(" ", "%20");
複製程式碼
  • 第四種方法和第一種方法異曲同工:先統計空白的數量,然後計算出有多少空白的長度,然後設定新的長度StringBuffer,通過插入最大角標的地方,不斷的插入,就可以了
 private String replaceSpaces(String string) {
        //判斷是否 輸入合法
        if (string == null || string.length() < 1) {
            return "";
        }
        char[] chars = string.toCharArray();
        // 統計有多少的空白的陣列
        int whiteCount = 0;
        for (int i = 0; i < chars.length; i++) {
            if (chars[i] == ' ') {
                whiteCount++;
            }
        }
        if (whiteCount == 0) {
            return string;
        }
        //最原本的長度
        int indexold = string.length() - 1;
        // 轉換完成後的長度
        int newlength = string.length() + whiteCount * 2;
        //新的長度
        int indexnew = newlength - 1;
        StringBuffer stringBuffer = new StringBuffer(string);
        //設定新的buffer的長度
        stringBuffer.setLength(newlength);
        for (; indexold >= 0 && indexold < newlength; indexold--) {
            // 原來的 字串的最後一位為空格
            if (string.charAt(indexold) == ' ') {
                stringBuffer.setCharAt(indexnew--, '0');
                stringBuffer.setCharAt(indexnew--, '2');
                stringBuffer.setCharAt(indexnew--, '%');
            } else {//不為空的話,就直接放進去 就行了
                stringBuffer.setCharAt(indexnew--, string.charAt(indexold));
            }
        }
        return stringBuffer.toString();
    }
複製程式碼

3、輸入個連結串列的頭結點,從尾到頭反過來列印出每個結點的值。

  • 資料結構
 public static class ListNode {
        int val;
        ListNode next;
        public ListNode(int  v){
            this.val=v;
        }
    }
複製程式碼
  • 初始化
     ListNode listNode = new ListNode(10);
     ListNode listNode1 = new ListNode(11);
     ListNode listNode2 = new ListNode(12);
     ListNode listNode3 = new ListNode(13);
      ListNode listNode4 = new ListNode(14);
        listNode.next=listNode1;
        listNode1.next=listNode2;
        listNode2.next=listNode3;
        listNode3.next=listNode4;
複製程式碼
  • 第一種方法的詳情,非常像二叉樹的遍歷,也是最簡單的方法。
   public static ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        arrayList.clear();
        if (listNode != null) {
            printListFromTailToHead(listNode.next);//指向下一個節點
            arrayList.add(listNode.val);//將當前節點的值存到列表中
        }
        return arrayList;
    }
複製程式碼
  • 第二種方法,利用Stack的特點,Stack類:繼承自Vector,實現一個後進先出的棧,不太明白的同學可以看這裡常用集合的原理分析;
private static void doWhat(ListNode listNode) {
        Stack<ListNode> stack = new Stack<>();
        while (listNode!=null){
            stack.push(listNode);
            listNode=listNode.next;
        }
        System.out.println(" stack "+stack.size());
//        for (int i=0;i<stack.size();i++){
//            System.out.println("每個該列印的元素 ::"+stack.get(i).val);
//        }
        ListNode tmp;
        while (!stack.empty()){
            // 移除堆疊頂部的物件,並作為此函式的值返回該物件。
            tmp = stack.pop();
            System.out.println("tmp="+tmp.val);
          //  System.out.println("每個該列印的元素 :"+tmp.val);
        }
    }
複製程式碼

4、輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。這個題的官方的答案,我本人持有嚴重的懷疑,也有可能是我根本沒有找到正確的答案!

  • 認識下什麼二叉樹:二叉樹是每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。二叉樹常被用於實現二叉查詢樹和二叉堆。樹的資料結構能夠同時具備陣列查詢快的優點以及連結串列插入和刪除快的優點
  • 程式碼結構
public interface Tree {
    //查詢節點
     Node find(int key);
    //插入新節點
     boolean insert(int data);
    //中序遍歷
     void infixOrder(Node current);
    //前序遍歷
     void preOrder(Node current);
    //後序遍歷
     void postOrder(Node current);
    //查詢最大值
     Node findMax();
    //查詢最小值
     Node findMin();
    //刪除節點
     boolean delete(int key);
}
複製程式碼
  • Node
public class Node {
    int data;   //節點資料
    Node leftChild; //左子節點的引用
    Node rightChild; //右子節點的引用

    public Node(int data){
        this.data = data;
    }
    //列印節點內容
    public void display(){
        System.out.println(data);
    }

    @Override
    public String toString() {
        return super.toString();
    }
}
複製程式碼
  • 插入資料的過程
      BinaryTree bt = new BinaryTree();
       // 第一個插入的結點是 根節點
       bt.insert(50);
       bt.insert(20);
       bt.insert(80);
       bt.insert(10);
       bt.insert(30);
       bt.insert(60);
       bt.insert(90);
       bt.insert(25);
       bt.insert(85);
       bt.insert(100);
複製程式碼
  • insert 程式碼
//插入節點
    public boolean insert(int data) {
        Node newNode = new Node(data);
        if (root == null) {//當前樹為空樹,沒有任何節點
            root = newNode;
            return true;
        } else {
            Node current = root;
            Node parentNode = null;
            while (current != null) {
                parentNode = current;
                if (current.data > newNode.data) {//當前值比插入值大,搜尋左子節點
                    current = current.leftChild;
                    if (current == null) {//左子節點為空,直接將新值插入到該節點
                        parentNode.leftChild = newNode;
                        return true;
                    }
                } else {
                    current = current.rightChild;
                    if (current == null) {//右子節點為空,直接將新值插入到該節點
                        parentNode.rightChild = newNode;
                        return true;
                    }
                }
            }
        }
        return false;
    }
複製程式碼

二叉樹插入資料的過程.png

二叉樹最終的結構.png

  • 最終的圖解如下

二叉樹 insert 圖解.jpg

  • 二叉樹的前序遍歷:根節點>>左子樹>>右子樹
  public void preOrder(Node current) {
        if (current != null) {
            System.out.print(current.data + " ");
            infixOrder(current.leftChild);
            infixOrder(current.rightChild);
        }
    }
複製程式碼
  • 1、根節點50,查詢左節點,找到20,然後找到10,輸出10,然後找到25 ,然後30 。到這裡輸出的結果是 50 10 20 25 30
  • 2、查詢根節點的50的右節點,然後找出80,找出80的左節點60.接著80,查詢80的右節點95,輸出85 然後 90 ,最後輸出100
  • 3、最終的輸出的結果50 10 20 25 30 60 80 85 90 100

前序遍歷.png

  • 二叉樹中序遍歷:首先遍歷左子樹,然後訪問根結點,最後遍歷右子樹。 左子樹 ---》根節點----》 右子樹
  public void infixOrder(Node current) {
        if (current != null) {
            infixOrder(current.leftChild);
            System.out.print(current.data + " ");
            infixOrder(current.rightChild);
        }
    }
複製程式碼
  • 1、傳入根節點 值為50 ,查詢50的左節點為20不為null,再次查詢20的左節點,為10,也不為null,然後在查詢10的左節點,為null ,輸出10,第一個節點輸出完成為 10
  • 2、接下來,查詢的10的右節點,為null,不進入輸出語句
  • 3、這下輸出語句20,查詢20右節點,查詢到值為30,第一步查詢的值為左節點,查詢到的值為25,查詢右節點為null
  • 4、到這裡輸出的結果為 10 20 25 30
  • 5、這樣子50左節點就全部查詢完了
  • 6、接下來查詢50的右節點,查詢右節點80,然後查詢80的左節點,查詢到的值為60,60沒有右節點,繼續查詢80的右節點,到這裡 輸出的結果10 20 25 30 50 60 80
  • 7、查詢80右節點,查到到值為90,接著查詢90的左節點,查到到的值85,接著85的左右節點為null,返回直接查詢90的右節點,查詢到了100,
  • 8、最終輸出的結果,10 20 25 30 50 60 80 85 90 100

二叉樹的中序遍歷.png

  • 後序遍歷:在二叉樹中,先左後右再根,即首先遍歷左子樹,然後遍歷右子樹,最後訪問根結點。
    • 1、根節點50,查詢左節點20,在繼續查詢20的左節點10,10沒有左右節點,列印10,然後查詢到20的右節點30,30繼續查詢到25,第一次輸出為 10 20 25 30
    • 2、最終輸出10 20 25 30 60 80 85 90 100 50

二叉樹的後序遍歷.png

  • 查詢最大值或者是最小值
 //找到最大值
    public Node findMax() {
        Node current = root;
        Node maxNode = current;
        while (current != null) {
            maxNode = current;
            current = current.rightChild;
        }
        return maxNode;
    }

    //找到最小值
    public Node findMin() {
        Node current = root;
        Node minNode = current;
        while (current != null) {
            minNode = current;
            current = current.leftChild;
        }
        return minNode;
    }
複製程式碼
  • 例如:前序遍歷序列{50 10 20 25 30 60 80 85 90 100}和中序遍歷序列{10 20 25 30 50 60 80 85 90 100}, 重建二叉樹並輸出它的頭結點。
  • 原始節點的遍歷結果,可以很清楚的看到,有多少左右節點,那個節點是那個的節點等等的資訊。
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node50  是那邊啊 根
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node20  是那邊啊 左
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node10  是那邊啊 左
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node30  是那邊啊 右
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node25  是那邊啊 左
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node80  是那邊啊 右
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node60  是那邊啊 左
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node90  是那邊啊 右
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node85  是那邊啊 左
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node100  是那邊啊 右
複製程式碼
  • 解決方法一:
 public static Node reConstructBinaryTree(int[] pre, int[] in) {
        if (pre == null || in == null || pre.length != in.length)//如果先序或者中序陣列有一個為空的話,就無法建樹,返回為空
            return null;
        else {
            return reBulidTree(pre, 0, pre.length - 1, in, 0, in.length - 1);
        }
    }

    /**
     * @param pre
     * @param startPre
     * @param endPre
     * @param in
     * @param startIn
     * @param endIn
     * @return
     */
    private static Node reBulidTree(int[] pre, int startPre, int endPre, int[] in, int startIn, int endIn) {
        if (startPre > endPre || startIn > endIn)//先對傳的引數進行檢查判斷
            return null;
        int root = pre[startPre];//陣列的開始位置的元素是跟元素
        int locateRoot = locate(root, in, startIn, endIn);//得到根節點在中序陣列中的位置 左子樹的中序和右子樹的中序以根節點位置為界

        if (locateRoot == -1) //在中序陣列中沒有找到跟節點,則返回空
            return null;
        Node treeRoot = new Node(root);//建立樹根節點
        treeRoot.leftChild = reBulidTree(pre, startPre + 1, startPre + locateRoot - startIn, in, startIn, locateRoot - 1);//遞迴構建左子樹
        treeRoot.rightChild = reBulidTree(pre, startPre + locateRoot - startIn + 1, endPre, in, locateRoot + 1, endIn);//遞迴構建右子樹
        return treeRoot;
    }

    //找到根節點在中序陣列中的位置,根節點之前的是左子樹的中序陣列,根節點之後的是右子樹的中序陣列
    private static int locate(int root, int[] in, int startIn, int endIn) {
        for (int i = startIn; i < endIn; i++) {
            if (root == in[i])
                return i;
        }
        return -1;
    }

複製程式碼
  • 但是這個的輸出結果如下,很明顯沒有重建二叉樹成功,它的執行流程如上面的程式碼,由於個人認為沒有重建,所以就沒有輸出示意圖
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 新新----的中序遍歷的開始
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node50  是那邊啊 根
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node10  是那邊啊 左
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node20  是那邊啊 右
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node25  是那邊啊 右
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node60  是那邊啊 右
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node80  是那邊啊 右
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node85  是那邊啊 右
09-02 05:51:53.177 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node90  是那邊啊 右
09-02 05:51:53.185 2590-2590/com.android.interview I/System.out: 新新----的中序遍歷的結束
複製程式碼
  • 解決方法二:解題思路也是不斷地遍歷,程式碼如下
 public static Node reConstructBinaryTreeNew(int[] pre, int[] in) {
        if (pre.length == 0 || in.length == 0)
            return null;
         Node node = new Node(pre[0]);
        for (int i = 0; i < pre.length; i++) {
            if (pre[0] == in[i]) {
                node.leftChild = reConstructBinaryTreeNew(Arrays.copyOfRange(pre, 1, i + 1), Arrays.copyOfRange(in, 0, i));
                node.rightChild = reConstructBinaryTreeNew(Arrays.copyOfRange(pre, i + 1, pre.length), Arrays.copyOfRange(in, i + 1, in.length));
                break;
            }
        }
        return node;
    }
複製程式碼
  • 輸出的結果和方法一的結果是一樣的。二叉樹的結果還是不一樣。
09-02 05:51:53.185 2590-2590/com.android.interview I/System.out: 新新----的中序遍歷的開始------------
09-02 05:51:53.185 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node50  是那邊啊 根
09-02 05:51:53.185 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node10  是那邊啊 左
09-02 05:51:53.186 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node20  是那邊啊 右
09-02 05:51:53.194 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node25  是那邊啊 右
09-02 05:51:53.211 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node30  是那邊啊 右
09-02 05:51:53.212 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node60  是那邊啊 右
09-02 05:51:53.212 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node80  是那邊啊 右
09-02 05:51:53.212 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node85  是那邊啊 右
09-02 05:51:53.213 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node90  是那邊啊 右
09-02 05:51:53.213 2590-2590/com.android.interview I/System.out: 這個值是什麼啊 Node100  是那邊啊 右
09-02 05:51:53.214 2590-2590/com.android.interview I/System.out: 新新----的中序遍歷的結束------------
複製程式碼
  • 解決方法三:由前序遍歷的第一個節點可知根節點。根據根節點,可以將中序遍歷劃分成左右子樹。在前序遍歷中找出對應的左右子樹,其第一個節點便是根節點的左右子節點。按照上述方式遞迴便可重建二叉樹。
public class Test {  
    /** 
     * 請實現一個函式,把字串中的每個空格替換成"%20",例如“We are happy.“,則輸出”We%20are%20happy.“。 
     * 
     * @param string     要轉換的字元陣列 
     * @param usedLength 已經字元陣列中已經使用的長度 
     * @return 轉換後使用的字元長度,-1表示處理失敗 
     */  
    public static int replaceBlank(char[] string, int usedLength) {  
        // 判斷輸入是否合法  
        if (string == null || string.length < usedLength) {  
            return -1;  
        }  
  
        // 統計字元陣列中的空白字元數  
        int whiteCount = 0;  
        for (int i = 0; i < usedLength; i++) {  
            if (string[i] == ' ') {  
                whiteCount++;  
            }  
        }  
  
        // 計算轉換後的字元長度是多少  
        int targetLength = whiteCount * 2 + usedLength;  
        int tmp = targetLength; // 儲存長度結果用於返回  
        if (targetLength > string.length) { // 如果轉換後的長度大於陣列的最大長度,直接返回失敗  
            return -1;  
        }  
  
        // 如果沒有空白字元就不用處理  
        if (whiteCount == 0) {  
            return usedLength;  
        }  
  
        usedLength--; // 從後向前,第一個開始處理的字元  
        targetLength--; // 處理後的字元放置的位置  
  
        // 字元中有空白字元,一直處理到所有的空白字元處理完  
        while (usedLength >= 0 && usedLength < targetLength) {  
            // 如是當前字元是空白字元,進行"%20"替換  
            if (string[usedLength] == ' ') {  
                string[targetLength--] = '0';  
                string[targetLength--] = '2';  
                string[targetLength--] = '%';  
            } else { // 否則移動字元  
                string[targetLength--] = string[usedLength];  
            }  
            usedLength--;  
        }  
  
        return tmp;  
    }  
}  
複製程式碼

相關文章