劍指offer刷題記錄(五)

1.

這道題確實有難度,因爲它的限制條件非常多了,所以難度大大提升了。我想到了遞歸,但這裏不讓用if語句來判斷何時跳出遞歸。

這裏貼出一個大佬的解法,使用了一個&&短路的特性,就是比如(關係式a)&&(關係式b),如果a爲真,就會去驗證b;但是如果a爲假,就不會去執行b了。所以就可以把遞歸主體放進b中,將遞歸跳出的條件放入a中。

class Solution {
    public int sumNums(int n) {
        int sum = n;
        boolean b = (n > 0) && ((sum += sumNums(n - 1)) > 0);
        return sum;
    }
}

可以看出這裏b的boolean值其實是沒有任何意義的,然後後一個關係式sum+=sumNums(n-1)>0,比較大小不是目的,目的是計算sum的累加值。

2.

這道題,最容易想到的就是用HashMap去儲存出現的每一個元素,如果該元素不在圖裏,那就添加進去,如果已經存在,那就value值+1。

使用HashMap的解法:容易理解簡單,但時間複雜度高

class Solution {
    public int singleNumber(int[] nums) {
        if(nums.length == 0) return 0;
        //創建一個哈希圖,來存放數組裏的元素
        Map<Integer,Integer> map = new HashMap<Integer,Integer>();
        for(int i=0;i<nums.length;i++){
            if(!map.containsKey(nums[i])){
                map.put(nums[i],1);
                continue;
            }
            map.put(nums[i],map.get(nums[i])+1);
        }
        int res = 0;
        for(Map.Entry<Integer,Integer> entry:map.entrySet()){
            if(entry.getValue() == 1){
                res = entry.getKey();
                break;
            }
        }
        return res;
    }
}

二進制逐位法:

class Solution {
    public int singleNumber(int[] nums) {
        if(nums.length == 0) return 0;
        int res = 0;//這個必須初始化爲0的
        for(int i=0;i<32;i++){
            int count = 0;
            int index = 1<<i;
            for(int n:nums){
                if((n & index) != 0) count++;             
            }
            if(count % 3 ==1) {//如果不能被3整除的話,那說明出現依次的數在這個位上爲1
                res ^= (1<<i);
            }
        }

        return res;
    }
}

3.

這道題和上一題有點像的,但如果空間複雜度要求是O(1)的話,那hashmap就沒法用了。所以像上一題一樣,這道題也得從位運算入手,同樣也是異或。下面貼一下思路和解法,我自己沒想出來

這裏 在整個數組裏異或的最終值其實就是出現1次的那兩個數的異或,所以最後a和b分別在兩組內做異或也是利用這一點。看到另一個解法裏,通過n&-n來尋找最先出現1的最低位。

class Solution {
    public int[] singleNumbers(int[] nums) {    
        //a,b分別代表出現次數爲1的兩個數
        int a=0,b=0,result=0;
        //最終:result=a^b
        for(int x:nums){
            result^=x;
        }
        int h=1;
        //找到result裏從低到高位中,第一個爲1的位
        //假如result=0100,那麼h=4
        while((result & h)==0){
            h*=2;
        }
        for(int x:nums){
            if((h & x)==0){
                a^=x;
            }else{
                b^=x;
            }
        }
        return new int[]{a,b};
    }
}

4.

這裏正好複習一下二叉樹,前序遍歷:根節點 左節點 右節點(前序遍歷的結果第一個數一定是總的根節點),中序遍歷:左節點 根節點 右節點,後序遍歷:左節點 右節點 根節點(最後一個一定是根節點)。

已知前序遍歷或後序遍歷可以得到根節點,中序遍歷在已知根節點的情況下可以得知左子樹和右子樹的遍歷結果,所以已知前序遍歷結果和中序遍歷結果、已知後序遍歷結果和中序比那裏結果都可以推導出二叉樹結構。但已知前序節點和後序節點不能推導出,因爲無法判斷左子樹和右子樹節點個數。

我的思路就是,先通過前序遍歷得知根節點,然後根據中序遍歷來劃分左子樹的中序遍歷和右子樹的中序遍歷,然後同時劃分出左子樹的前序遍歷和右子樹的前序遍歷,之後就是遞歸了。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode buildTree(int[] prev, int[] in) {
        if(prev.length!= in.length || prev.length<1){
            return null;
        }
	//只有一個節點,那就是根節點
        if(prev.length == 1){
            return new TreeNode(prev[0]);
        }
	//在中序遍歷結果中找根節點
        int index = -1;
        for(int i=0;i<in.length;i++){
            if(in[i]==prev[0]){
                index=i;
                break;
            }
        }
	//沒找到,說明數據有問題
        if(index==-1){
            return null;
        }
	//找到根節點了
        TreeNode root = new TreeNode(prev[0]);
	//得到左子樹的前序遍歷結果
        int[] lChildPrev = new int[index];
        System.arraycopy(prev,1,lChildPrev,0,index);
	//得到左子樹的中序遍歷結果
        int[] lChildin = new int[index];
        System.arraycopy(in,0,lChildin,0,index);
	//通過遞歸,得到左子樹結構
        root.left=buildTree(lChildPrev,lChildin);
        
	//得到右子樹的前序遍歷結果
        int[] rChildPrev = new int[in.length-1-index];
        System.arraycopy(prev,index+1,rChildPrev,0,in.length-1-index);
	//得到右子樹的中序遍歷結果
        int[] rChildin = new int[in.length-1-index];
        System.arraycopy(in,index+1,rChildin,0,in.length-1-index);
	//通過遞歸,得到右子樹結構
        root.right=buildTree(rChildPrev,rChildin);
	//得到完整的二叉樹結構
        return root;
    }
}

大佬的解法:使用了一個hashmap來儲存中序遍歷的索引與值的一一對應的情況,方便我們從前序遍歷中得到根節點後在中序遍歷中尋找。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    HashMap<Integer, Integer> dic = new HashMap<>();
    int[] po;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        po = preorder;
        for(int i = 0; i < inorder.length; i++) 
            dic.put(inorder[i], i);
        return recur(0, 0, inorder.length - 1);
    }
    TreeNode recur(int pre_root, int in_left, int in_right) {
        if(in_left > in_right) return null;
        TreeNode root = new TreeNode(po[pre_root]);
        int i = dic.get(po[pre_root]);
        root.left = recur(pre_root + 1, in_left, i - 1);
        root.right = recur(pre_root + i - in_left + 1, i + 1, in_right);
        return root;
    }
}

5.

這道題是典型的動態規劃題,遇到動態規劃,一定找出它的狀態定義、轉移方程、初始狀態。然後再看是否可以優化一些流程。

class Solution {
    public int maxValue(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(i==0 && j==0) continue;
                if(i==0){
                    grid[i][j] += grid[i][j-1];
                }
                else if(j==0){
                    grid[i][j] += grid[i-1][j];
                }else{
                    grid[i][j] +=Math.max(grid[i-1][j],grid[i][j-1]);
                }              
            }
        }
        return grid[m-1][n-1];
 
    }
}

當grid很大的時候,相對來說判定第一行和第一列所佔的比例較少,所以可以把它們單獨拿出來,不需要每次循環完都進行一次判定。

class Solution {
    public int maxValue(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        for(int j = 1; j < n; j++) // 初始化第一行
            grid[0][j] += grid[0][j - 1];
        for(int i = 1; i < m; i++) // 初始化第一列
            grid[i][0] += grid[i - 1][0];
        for(int i = 1; i < m; i++)
            for(int j = 1; j < n; j++) 
                grid[i][j] += Math.max(grid[i][j - 1], grid[i - 1][j]);
        return grid[m - 1][n - 1];
    }
}

6.

這道題一看就有一種似曾相識的感覺,和之前做的這一題非常像。

其實這分成兩道題,其實就是一道題。

所以思路一:觀察它的打印順序,必須是一層一層遞進下來,所以二叉樹層次遍歷法非常適合解決這道題。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public int[] levelOrder(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        ArrayList<Integer> list = new ArrayList<>();//用來存放
        if(root == null) return new int[0];
        queue.add(root);
        while(!queue.isEmpty()){
            TreeNode node = queue.poll();
            list.add(node.val);
            if(node.left != null) queue.add(node.left);
            if(node.right != null) queue.add(node.right);
        }
        int[] res = new int[list.size()];
        for(int i=0;i<list.size();i++){
            res[i] = list.get(i);
        }
        return res;
    }  
}

這道題最後有一個細節,就是Arraylist怎麼轉化成Int[]數組,如果是string[]數組是很好轉化的,但這裏是Int[]數組就不能直接使用toArray()方法,會報錯。所以我就採用了一個for循環的方法,可以使得ArrayList中每一個元素加到int[]數組中。除了這個辦法就是使用流,但好像很複雜。

7.

這道題給我第一個感覺就是使用動態規劃,因爲前後天的最大利潤是有關係的,就是我一開始找的關係有點問題,沒有找到關鍵點。這裏比如第i天的最大利潤dp[i],它和dp[i-1]是有一定的關係的。注意dp[i]表示是以prices[i]爲結尾的子數組的最大利潤,並不是表示第i天賣出的最大利潤,就是大數組分成小數組去考慮問題。

所以就可以找出其中的關係,dp[i] = Math.max(dp[i-1],prices[i] - min); 這裏min就代表prices數組中從0到i-1,這些元素中的最小值。

所以很快我就把這個思路的動態規劃寫出來了。

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length == 0) return 0;
        int[] dp = new int[prices.length];
        dp[0] = 0;
        int min = prices[0];
        for(int i=1;i<prices.length;i++){
            if(prices[i] < min)  min = prices[i];
            dp[i] = Math.max(dp[i-1],prices[i] - min); 
        } 
        return dp[prices.length-1];   
    }
}

看了大佬的優化思路:

class Solution {
    public int maxProfit(int[] prices) {
        int cost = Integer.MAX_VALUE, profit = 0;
        for(int price : prices) {
            cost = Math.min(cost, price);
            profit = Math.max(profit, price - cost);
        }
        return profit;
    }
}

8.

這道題沒有想到動態規劃,是看到了大佬的解法,豁然開朗的,才明白此處用動態規劃竟然如此簡單。因爲從第2個數開始,所有的數,一定是前面的某個數*2或者*3或者*5,例如2,3,5,6,8,皆是如此。所以思路如下:

實現的代碼如下:

class Solution {
    public int nthUglyNumber(int n) {
        int a=0,b=0,c=0;
        int[] dp = new int[n];//用於存放醜數
        dp[0] = 1;
        for(int i=1;i<n;i++){
            dp[i] = Math.min(Math.min(dp[a]*2,dp[b]*3),dp[c]*5);
            if(dp[i] == dp[a]*2) a+=1;
            if(dp[i] == dp[b]*3) b+=1;
            if(dp[i] == dp[c]*5) c+=1;
        }
        return dp[n-1];
    }
}

9.

思路:因爲它本身是一個二叉搜索樹,根節點大於左節點,小於右節點。所以中序遍歷正好可以讓它輸出保持一個遞增的情況。下面貼上大佬的思路:

代碼實現如下:

/*
// Definition for a Node.
class Node {
    public int val;
    public Node left;
    public Node right;

    public Node() {}

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val,Node _left,Node _right) {
        val = _val;
        left = _left;
        right = _right;
    }
};
*/
class Solution {
    Node pre, head;
    public Node treeToDoublyList(Node root) {
        if(root == null) return null;
        dfs(root);
        head.left = pre;
        pre.right = head;
        return head;
    }
    void dfs(Node cur) {
        if(cur == null) return;
        dfs(cur.left);
        if(pre != null) pre.right = cur;
        else head = cur;
        cur.left = pre;
        pre = cur;
        dfs(cur.right);
    }
}

10.

解法一:層次遍歷+倒序

層次遍歷不用多說了,之前兩道類似的題已經遇到過了,使用隊列依次將元素壓入隊列中。這裏在每次循環之前先獲取隊列size,這個size就是這一行的元素個數,然後來循環推出元素,確保每一行的元素完整推出。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        if (root == null) {
            return res;
        }
        int row = 1;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while(!queue.isEmpty()){
            int size = queue.size();//獲取這一層有幾個
            List<Integer> list = new ArrayList<>();//用來存放這一層的數據
            for(int i=0;i<size;i++){
                TreeNode node = queue.poll();
                list.add(node.val);
                if(node.left != null) queue.add(node.left);
                if(node.right != null) queue.add(node.right);                     
            }
            if(row % 2 == 0){
                Collections.reverse(list);
            }
            res.add(list);
            row++;
        }
        return res;
    }
}

法二:使用雙向隊列,因爲雙向隊列是可以從頭或者尾部彈出元素的,這樣就不用倒序了

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        Deque<TreeNode> deque = new LinkedList<>();
        List<List<Integer>> res = new ArrayList<>();
        if(root != null) deque.add(root);
        while(!deque.isEmpty()) {
            // 打印奇數層
            List<Integer> tmp = new ArrayList<>();
            for(int i = deque.size(); i > 0; i--) {
                // 從左向右打印
                TreeNode node = deque.removeFirst();
                tmp.add(node.val);
                // 先左後右加入下層節點
                if(node.left != null) deque.addLast(node.left);
                if(node.right != null) deque.addLast(node.right);
            }
            res.add(tmp);
            if(deque.isEmpty()) break;
            // 打印偶數層
            tmp = new ArrayList<>();
            for(int i = deque.size(); i > 0; i--) {
                // 從右向左打印
                TreeNode node = deque.removeLast();
                tmp.add(node.val);
                // 先右後左加入下層節點
                if(node.right != null) deque.addFirst(node.right);
                if(node.left != null) deque.addFirst(node.left);
            }
            res.add(tmp);
        }
        return res;
    }
}

對比下來,兩者的時間複雜度和空間複雜度是差不多的,如果面試時不讓使用reverse函數,那就用這個雙向隊列。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章