剑指OFFER

剑指OFFER部分题解

        • 23. 链表中环的入口结点
          • 题目描述
          • 解题思路
        • 子树结构
        • 树镜像
        • 矩阵中的路径
          • 题目描述
          • 思路
        • 13. 机器人的运动范围
            • 题目描述
          • 解题思路
        • 14. 剪绳子(dp或贪心)
          • 题目描述
          • 思路
        • 数值的整数次方
          • 题目描述
        • 删除链表中重复的节点(使用头结点)
        • 20. 表示数值的字符串(正则表达式的使用)
          • 解题思路:使用正则表达式
        • 32.3 按之字形顺序打印二叉树
        • 二叉搜索树的后续遍历序列(判断序列是否为二叉搜索树的后续遍历序列)
        • 34. 二叉树中和为某一值的路径
          • 题目描述
        • 复杂链表的复制
          • 题目描述
        • 36. 二叉搜索树与双向链表
        • 38. 字符串的排列
          • 题目描述
        • 最小的K个数(用PriorityQueue实现最大堆)
        • 41.1 数据流中的中位数
          • 题目描述
          • 解题思路
        • 41.2 字符流中第一个不重复的字符
        • 43. 从 1 到 n 整数中 1 出现的次数
        • 45. 把数组排成最小的数
        • 46. 把数字翻译成字符串
        • 约瑟夫环
        • 47. 礼物的最大价值(dp路径问题:1.常规,2.空间压缩)
        • 题目:最长不含重复字符的子字符串
        • 丑数
        • 52. 两个链表的第一个公共结点
        • 57.2 和为 S 的连续正数序列
        • 58.1 翻转单词顺序列
        • 58.2 左旋转字符串
        • 59. 滑动窗口的最大值
        • n 个骰子的点数(dp)
        • 63. 股票的最大利润
        • 不用加减乘除做加法
        • 68. 树中两个节点的最低公共祖先

####重建二叉树(通过树的前序遍历和中序遍历)

/**
 * Definition for binary tree
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {


    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        TreeNode root=reConstructBinaryTree(pre,0,pre.length-1,in,0,in.length-1);
        return root;
    }
    public TreeNode reConstructBinaryTree(int [] pre,int startpre,int endpre,int [] in,int startin,int endin) {
        if(startpre>endpre||startin>endin){
            return null;
        }
        TreeNode root = new TreeNode(pre[startpre]);
        for(int i=startin;i<=endin;i++){
            if(in[i]==pre[startpre]){
                root.left = reConstructBinaryTree(pre,startpre+1,i-startin+startpre,in,startin,i-1);
                root.right = reConstructBinaryTree(pre,i-startin+startpre+1,endpre,in,i+1,endin);
                break;
            }
        }
        return root;
    }
}

23. 链表中环的入口结点

题目描述

一个链表中包含环,请找出该链表的环的入口结点。要求不能使用额外的空间。

解题思路
  • 使用双指针,一个指针 fast 每次移动两个节点,一个指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。假设相遇点在下图的 z1 位置,此时 fast 移动的节点数为 x+2y+z,slow 为 x+y,由于 fast 速度比 slow 快一倍,因此 x+2y+z=2(x+y),得到 x=z。

  • 在相遇点,slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z,因此 fast 和 slow 将在环入口点相遇。
    剑指OFFER_第1张图片

public ListNode EntryNodeOfLoop(ListNode pHead) {
    if (pHead == null || pHead.next == null)
        return null;
    ListNode slow = pHead, fast = pHead;
    do {
        fast = fast.next.next;
        slow = slow.next;
    } while (slow != fast);
    fast = pHead;
    while (slow != fast) {
        slow = slow.next;
        fast = fast.next;
    }
    return slow;
}

子树结构

/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    public boolean HasSubtree(TreeNode root1,TreeNode root2) {
        if(root1==null||root2==null){
            return false;
        }
        return judge(root1,root2)||HasSubtree(root1.left,root2)||HasSubtree(root1.right,root2);
    }
    public boolean judge(TreeNode root1,TreeNode root2){
        if(root2==null){
            return true;
        }
        if(root1==null){
            return false;
        }
        if(root1.val==root2.val){
            return judge(root1.left,root2.left)&&judge(root1.right,root2.right);
        }
        return false;
    }
}

树镜像

    /**
    public class TreeNode {
        int val = 0;
        TreeNode left = null;
        TreeNode right = null;

        public TreeNode(int val) {
            this.val = val;

        }

    }
    */
    public class Solution {
        public void Mirror(TreeNode root) {
            if(root==null)
                return ;
            swap(root);
            Mirror(root.left);
            Mirror(root.right);
        }
        public void swap(TreeNode root){
            TreeNode t = root.left;
            root.left = root.right;
            root.right = t;
        }
    }

矩阵中的路径

题目描述
  • 请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bccced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。
思路
  • 使用回溯法(backtracking)进行求解, 它是一种暴力搜索方法,通过搜索所有可能的结果来求解问题。回溯法在一次搜索结束时需要进行回溯(回退),将这一次搜索过程中设置的状态进行清除,从而开始一次新的搜索过程。例如下图示例中,从 f 开始,下一步有 4 种搜索可能,如果先搜索 b,需要将 b 标记为已经使用,防止重复使用。在这一次搜索结束之后,需要将 b 的已经使用状态清除,并搜索 c。
    剑指OFFER_第2张图片
public class Solution {
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str)
    {
        boolean [][] flags = new boolean[rows][cols];
        for(int i=0;i<rows;i++){
            for(int j=0;j<cols;j++){
                if(pathFind(matrix,i,j,rows,cols,flags,str,0)){
                    return true;
                }
            }
        }
        return false;
    }
    boolean pathFind(char[] matrix, int i, int j, int rows, int cols,boolean[][]flags, char str[], int k){
        int index = i*cols+j;
        if(i<0||j<0||i>=rows||j>=col*matrix[index]!=str[k]||flags[i][j]==true){
            return false;
        }
        if(k==str.length-1){return true;}
        flags[i][j]=true;
        if(pathFind(matrix,i-1,j,rows,cols,flags,str,k+1)||
          pathFind(matrix,i+1,j,rows,cols,flags,str,k+1)||
          pathFind(matrix,i,j-1,rows,cols,flags,str,k+1)||
          pathFind(matrix,i,j+1,rows,cols,flags,str,k+1)) return true;
        flags[i][j]=false;
        return false;
    }
 
}

13. 机器人的运动范围

题目描述
  • 地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。例如,当 k 为 18 时,机器人能够进入方格 (35,37),因为 3+5+3+7=18。但是,它不能进入方格 (35,38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子?
解题思路
  • 使用深度优先搜索(Depth First Search,DFS)方法进行求解。回溯是深度优先搜索的一种特例,它在一次搜索过程中需要设置一些本次搜索过程的局部状态,并在本次搜索结束之后清除状态。而普通的深度优先搜索并不需要使用这些局部状态,虽然还是有可能设置一些全局状态。
public class Solution {
    public int movingCount(int threshold, int rows, int cols)
    {
        int flag[][] = new int[rows][cols];
        return helper(threshold,rows,cols,0,0,flag);
    }
    public int helper(int threshold, int rows, int cols,int i,int j,int flag[][]){
        if(i<0||j<0||i>=rows||j>=cols||sum(i)+sum(j)>threshold||flag[i][j]==1){
            return 0;
        }
        flag[i][j] = 1;
        return helper(threshold,rows,cols,i-1,j,flag)
                +helper(threshold,rows,cols,i+1,j,flag)
                +helper(threshold,rows,cols,i,j-1,flag)
                +helper(threshold,rows,cols,i,j+1,flag)+1;
    }

    public int sum(int i){
        int sum = 0;
        while(i!=0){
            sum+=i%10;
            i/=10;
        }
        return sum;
    }
}

14. 剪绳子(dp或贪心)

题目描述

把一根绳子剪成多段,并且使得每段的长度乘积最大。

思路
  • dp[i] = max(dp[i],dp[j]*dp[i-j])

  • 贪心
    尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现。如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。

    证明:当 n >= 5 时,3(n - 3) - n = 2n - 9 > 0,且 2(n - 2) - n = n - 4 > 0。因此在 n >= 5 的情况下,将绳子剪成一段为 2 或者 3,得到的乘积会更大。又因为 3(n - 3) - 2(n - 2) = n - 5 >= 0,所以剪成一段长度为 3 比长度为 2 得到的乘积更大。

class Solution {
    public int integerBreak(int n) {
        
        int []dp = new int[n+1];

        if(n<=3){
            return n-1;
        }
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 3;
        for(int i=4;i<=n;i++){
            for(int j=1;j<=i/2;j++){
                dp[i] = Math.max(dp[i],dp[j]*dp[i-j]);
            }
        }
        return dp[n];
        
    }
}
//贪心
public int integerBreak(int n) {
    if (n < 2)
        return 0;
    if (n == 2)
        return 1;
    if (n == 3)
        return 2;
    int timesOf3 = n / 3;
    if (n - timesOf3 * 3 == 1)
        timesOf3--;
    int timesOf2 = (n - timesOf3 * 3) / 2;
    return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2));
}

数值的整数次方

题目描述

给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。

保证base和exponent不同时为0

public class Solution {
    public double Power(double base, int exponent) {
        int n = exponent>0 ? exponent : -exponent;
        double res = 1.0;
        while(n!=0){
            if((n&1)==1){
                res*=base;
            }
            base = base*base;
            n = n>>1;
        }
        return exponent>0 ?res:1/res;
        
  }
}

删除链表中重复的节点(使用头结点)

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

/*
 public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ListNode deleteDuplication(ListNode pHead)
    {
        if(pHead==null||pHead.next==null){
            return pHead;
        }
        ListNode fir = new ListNode(-1);
        fir.next = pHead;
        ListNode p = pHead;
        ListNode last = fir;
        while(p!=null&&p.next!=null){
            if(p.val==p.next.val){
                int val=p.val;
                while(p!=null&&val==p.val){
                    p = p.next;
                }
                last.next = p;
            }else{
                last = p;
                p = p.next;
            }
        }
        return fir.next;
    }
}

20. 表示数值的字符串(正则表达式的使用)

  • 请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100",“5e2”,"-123",“3.1416"和”-1E-16"都表示数值。 但是"12e",“1a3.14”,“1.2.3”,"±5"和"12e+4.3"都不是。
解题思路:使用正则表达式
[]  : 字符集合
()  : 分组
?   : 重复 0 ~ 1 次
+   : 重复 1 ~ n 次
*   : 重复 0 ~ n 次
.   : 任意字符
\\. : 转义后的 .
\\d : 数字
public boolean isNumeric(char[] str) {
    if (str == null || str.length == 0)
        return false;
    return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?");
}

32.3 按之字形顺序打印二叉树

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Collections;
import java.util.*;
/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;

    }

}
*/
public class Solution {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {    
        ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
        if(pRoot==null)
            return ret;
        Stack<TreeNode> s1 = new Stack<>();//奇数层节点
        Stack<TreeNode> s2 = new Stack<>();//偶数层节点
        s1.push(pRoot);
        boolean isJi = true;
        while(!s1.isEmpty()||!s2.isEmpty()){
            if(isJi){
                ArrayList<Integer> list = new ArrayList();
                while(!s1.isEmpty()){
                    TreeNode t= s1.pop();
                    if(t!=null){
                        list.add(t.val);
                        s2.push(t.left);//偶数层节点从左往右入栈
                        s2.push(t.right);
                    }
                }
                if(!list.isEmpty()){
                    ret.add(list);
                }
            }else{
                ArrayList<Integer> list = new ArrayList();
                while(!s2.isEmpty()){
                    TreeNode t= s2.pop();
                    if(t!=null){
                        list.add(t.val);
                        s1.push(t.right);//奇数层节点从右往左入栈
                        s1.push(t.left);
                    }
                }
                if(!list.isEmpty()){
                    ret.add(list);
                }
            }
            isJi = !isJi;//奇偶翻转
        }
        return ret;

    }

}
public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
    ArrayList<ArrayList<Integer>> ret = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(pRoot);
    boolean reverse = false;
    while (!queue.isEmpty()) {
        ArrayList<Integer> list = new ArrayList<>();
        int cnt = queue.size();
        while (cnt-- > 0) {
            TreeNode node = queue.poll();
            if (node == null)
                continue;
            list.add(node.val);
            queue.add(node.left);
            queue.add(node.right);
        }
        if (reverse)
            Collections.reverse(list);
        reverse = !reverse;
        if (list.size() != 0)
            ret.add(list);
    }
    return ret;
}

二叉搜索树的后续遍历序列(判断序列是否为二叉搜索树的后续遍历序列)

题目描述
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同

public class Solution {
    public boolean VerifySquenceOfBST(int [] sequence) {
        if(sequence==null||sequence.length==0)
            return false;
        return judge(sequence,0,sequence.length-1);
    }
    public boolean judge(int[]sequence,int l,int r){
        if(l>=r){
            return true;
        }
        int last = sequence[r];
        int i=l;
        while(i<r&&sequence[i]<last)
            i++;
        for(int j=i;j<r;j++){
            if(sequence[j]<last){
                return false;
            }
        }
        return judge(sequence,l,i-1)&&judge(sequence,i,r-1);
        
    }
}

34. 二叉树中和为某一值的路径

题目描述

输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。

下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12

剑指OFFER_第3张图片

private ArrayList<ArrayList<Integer>> ret = new ArrayList<>();

public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
    backtracking(root, target, new ArrayList<>());
    return ret;
}

private void backtracking(TreeNode node, int target, ArrayList<Integer> path) {
    if (node == null)
        return;
    path.add(node.val);
    target -= node.val;
    if (target == 0 && node.left == null && node.right == null) {
        ret.add(new ArrayList<>(path));
    } else {
        backtracking(node.left, target, path);
        backtracking(node.right, target, path);
    }
    path.remove(path.size() - 1);
}

复杂链表的复制

题目描述

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

/*
public class RandomListNode {
    int label;
    RandomListNode next = null;
    RandomListNode random = null;

    RandomListNode(int label) {
        this.label = label;
    }
}
*/
public class Solution {
    public RandomListNode Clone(RandomListNode pHead)
    {
        if(pHead==null){
            return null;
        }
        
        RandomListNode cur = pHead;
        RandomListNode clone;
        while(cur!=null){
            clone = new RandomListNode(cur.label);
            clone.next = cur.next;
            cur.next = clone;
            cur = clone.next;
        }

        cur = pHead;
        while(cur!=null){
            clone = cur.next;
            if(cur.random!=null){
                clone.random = cur.random.next;
            }
            cur = clone.next;
        }
        cur = pHead;
        RandomListNode pCloneHead = pHead.next;
        while(cur!=null){
            clone = cur.next;
            cur.next = clone.next;
            clone.next = clone.next==null?null:clone.next.next;
            cur = cur.next;
        }
        return pCloneHead;

    }
}

36. 二叉搜索树与双向链表

  • 题目描述
    输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
private TreeNode pre = null;
private TreeNode head = null;

public TreeNode Convert(TreeNode root) {
    inOrder(root);
    return head;
}

private void inOrder(TreeNode node) {
    if (node == null)
        return;
    inOrder(node.left);
    node.left = pre;
    if (pre != null)
        pre.right = node;
    pre = node;
    if (head == null)
        head = node;
    inOrder(node.right);
}

38. 字符串的排列

题目描述

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。

private ArrayList<String> ret = new ArrayList<>();

public ArrayList<String> Permutation(String str) {
    if (str.length() == 0)
        return ret;
    char[] chars = str.toCharArray();
    Arrays.sort(chars);
    backtracking(chars, new boolean[chars.length], new StringBuilder());
    return ret;
}

private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) {
    if (s.length() == chars.length) {
        ret.add(s.toString());
        return;
    }
    for (int i = 0; i < chars.length; i++) {
        if (hasUsed[i])
            continue;
        if (i != 0 && chars[i] == chars[i - 1] && !hasUsed[i - 1]) /* 保证不重复 */
            continue;
        hasUsed[i] = true;
        s.append(chars[i]);
        backtracking(chars, hasUsed, s);
        s.deleteCharAt(s.length() - 1);
        hasUsed[i] = false;
    }
}

最小的K个数(用PriorityQueue实现最大堆)

  • 题目描述
    输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。
import java.util.TreeSet;
import java.util.Set;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.PriorityQueue;
public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        if(input.length<k||k<=0){
            return new ArrayList<Integer>();
        }
        PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1);
        int i=0;
        for(int num:input){
            heap.add(num);
            if(i>=k){
                heap.poll();
            }
            i++;
        }
        return new ArrayList<Integer>(heap);
        
        /*ArrayList list = new ArrayList<>();
        Set set = new TreeSet<>();
        int len = input.length;
        if(len
    }
}

41.1 数据流中的中位数

题目描述

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

解题思路
/* 大顶堆,存储左半边元素 */
private PriorityQueue<Integer> left = new PriorityQueue<>((o1, o2) -> o2 - o1);
/* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */
private PriorityQueue<Integer> right = new PriorityQueue<>();
/* 当前数据流读入的元素个数 */
private int N = 0;

public void Insert(Integer val) {
    /* 插入要保证两个堆存于平衡状态 */
    if (N % 2 == 0) {
        /* N 为偶数的情况下插入到右半边。
         * 因为右半边元素都要大于左半边,但是新插入的元素不一定比左半边元素来的大,
         * 因此需要先将元素插入左半边,然后利用左半边为大顶堆的特点,取出堆顶元素即为最大元素,此时插入右半边 */
        left.add(val);
        right.add(left.poll());
    } else {
        right.add(val);
        left.add(right.poll());
    }
    N++;
}

public Double GetMedian() {
    if (N % 2 == 0)
        return (left.peek() + right.peek()) / 2.0;
    else
        return (double) right.peek();
}

41.2 字符流中第一个不重复的字符

题目描述
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"

private int[] cnts = new int[256];
private Queue<Character> queue = new LinkedList<>();

public void Insert(char ch) {
    cnts[ch]++;
    queue.add(ch);
    while (!queue.isEmpty() && cnts[queue.peek()] > 1)
        queue.poll();
}

public char FirstAppearingOnce() {
    return queue.isEmpty() ? '#' : queue.peek();
}

43. 从 1 到 n 整数中 1 出现的次数

public int NumberOf1Between1AndN_Solution(int n) {
    int cnt = 0;
    for (int m = 1; m <= n; m *= 10) {
        int a = n / m, b = n % m;
        cnt += (a + 8) / 10 * m + (a % 10 == 1 ? b + 1 : 0);
    }
    return cnt;
}

45. 把数组排成最小的数

题目描述
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组 {3,32,321},则打印出这三个数字能排成的最小数字为 321323。

解题思路
可以看成是一个排序问题,在比较两个字符串 S1 和 S2 的大小时,应该比较的是 S1+S2 和 S2+S1 的大小,如果 S1+S2 < S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。

/*
* 解题思路:
 * 先将整型数组转换成String数组,然后将String数组排序,最后将排好序的字符串数组拼接出来。关键就是制定排序规则。
 * 排序规则如下:
 * 若ab > ba 则 a > b,
 * 若ab < ba 则 a < b,
 * 若ab = ba 则 a = b;
 * 解释说明:
 * 比如 "3" < "31"但是 "331" > "313",所以要将二者拼接起来进行比较
 * */
import java.util.Arrays;
import java.util.Comparator;

public class Solution {
    public String PrintMinNumber(int [] numbers) {
        if(numbers.length==0||numbers==null){
            return "";
        }
        String []num = new String[numbers.length];
        for(int i=0;i<numbers.length;i++){
            num[i] = numbers[i] + "";
        }
        Arrays.sort(num,new Comparator<String>(){
            @Override
            public int compare(String s1, String s2) {
                String c1 = s1 + s2;
                String c2 = s2 + s1;
                return c1.compareTo(c2);
            }
        }); 
        String str = "";
        for(int i=0;i<num.length;i++){
            str+=num[i];
        }
        return str;
    }
}

46. 把数字翻译成字符串

Leetcode

题目描述
给定一个数字,按照如下规则翻译成字符串:1 翻译成“a”,2 翻译成“b”… 26 翻译成“z”。一个数字有多种翻译可能,例如 12258 一共有 5 种,分别是 abbeh,lbeh,aveh,abyh,lyh。实现一个函数,用来计算一个数字有多少种不同的翻译方法。

解题思路
给定一个数字,按照如下规则翻译成字符串:1 翻译成“a”,2 翻译成“b”… 26 翻译成“z”。一个数字有多种翻译可能,例如 12258 一共有 5 种,分别是 abbeh,lbeh,aveh,abyh,lyh。实现一个函数,用来计算一个数字有多少种不同的翻译方法。

class Solution {
    public int numDecodings(String s) {
        if (s.charAt(0) == '0') return 0;
        
        int[] dp = new int[s.length() + 1];
        dp[0] = dp[1] = 1;
        
        for (int i = 2; i <= s.length(); i++) {
            //如果该位不为'0',说明该位单独成字母合法
            if (s.charAt(i - 1) != '0') { 
                dp[i] += dp[i - 1];
            }
            //如果后两位能组成"1x"(x为任意数字)或者"2x"(x小于7),说明最后两位组成字母合法
            if ((s.charAt(i - 2) == '1') || (s.charAt(i - 2) == '2' && s.charAt(i - 1) <= '6')) {
                dp[i] += dp[i - 2];
            }
        }
        return dp[s.length()];
    }
}


解法三 动态规划
同样的,递归就是压栈压栈压栈,出栈出栈出栈的过程,我们可以利用动态规划的思想,省略压栈的过程,直接从 bottom 到 top。

用一个 dp 数组, dp [ i ] 代表字符串 s [ i, s.len-1 ],也就是 s 从 i 开始到结尾的字符串的解码方式。

这样和递归完全一样的递推式。

如果 s [ i ] 和 s [ i + 1 ] 组成的数字小于等于 26,那么

dp [ i ] = dp[ i + 1 ] + dp [ i + 2 ]

public int numDecodings(String s) {
    int len = s.length();
    int[] dp = new int[len + 1];
    dp[len] = 1; //将递归法的结束条件初始化为 1 
    //最后一个数字不等于 0 就初始化为 1
    if (s.charAt(len - 1) != '0') {
        dp[len - 1] = 1;
    }
    for (int i = len - 2; i >= 0; i--) {
        //当前数字时 0 ,直接跳过,0 不代表任何字母
        if (s.charAt(i) == '0') {
            continue;
        }
        int ans1 = dp[i + 1];
        //判断两个字母组成的数字是否小于等于 26
        int ans2 = 0;
        int ten = (s.charAt(i) - '0') * 10;
        int one = s.charAt(i + 1) - '0';
        if (ten + one <= 26) {
            ans2 = dp[i + 2];
        }
        dp[i] = ans1 + ans2;

    }
    return dp[0];
}

约瑟夫环

思路:https://blog.csdn.net/wusuopubupt/article/details/18214999

public class Solution {
    public int LastRemaining_Solution(int n, int m) {
        if(n<=0||m<=0){
            return -1;
        }
        int ret = 0;
        for(int i=2;i<=n;i++){
            ret = (ret+m)%i;
        }
        return ret;
    }
}

47. 礼物的最大价值(dp路径问题:1.常规,2.空间压缩)

NowCoder

题目描述
在一个 m*n 的棋盘的每一个格都放有一个礼物,每个礼物都有一定价值(大于 0)。从左上角开始拿礼物,每次向右或向下移动一格,直到右下角结束。给定一个棋盘,求拿到礼物的最大价值。例如,对于如下棋盘。礼物的最大价值为 1+12+5+7+7+16+5=53。

1    10   3    8
12   2    9    6
5    7    4    11
3    7    16   5
链接:https://www.nowcoder.com/questionTerminal/72a99e28381a407991f2c96d8cb238ab
来源:牛客网

class Bonus {
public:
    int dp[6][6];
    int getMost(vector<vector<int> > board) {
        memset(dp, 0, sizeof(dp));
        dp[0][0] = board[0][0];
        for(int i = 1; i < 6; i++) dp[0][i] = dp[0][i-1] + board[0][i];
        for(int i = 1; i < 6; i++) dp[i][0] = dp[i-1][0] + board[i][0];
        for(int i = 1; i < 6; i++)
            for(int j = 1; j < 6; j++){
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + board[i][j];
            }
        return dp[5][5];
    }
};
public int getMost(int[][] values) {
    if (values == null || values.length == 0 || values[0].length == 0)
        return 0;
    int n = values[0].length;
    int[] dp = new int[n];
    for (int[] value : values) {
        dp[0] += value[0];
        for (int i = 1; i < n; i++)
            dp[i] = Math.max(dp[i], dp[i - 1]) + value[i];
    }
    return dp[n - 1];
}

题目:最长不含重复字符的子字符串

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。假设字符串中只包含从’a’到’z’的字符。例如,在字符串中”arabcacfr”,最长非重复子字符串为”acfr”,长度为4。

主要思路:使用动态规划,记录当前字符之前的最长非重复子字符串长度f(i-1),其中i为当前字符的位置。每次遍历当前字符时,分两种情况:

1)若当前字符第一次出现,则最长非重复子字符串长度f(i) = f(i-1)+1。
2)若当前字符不是第一次出现,则首先计算当前字符与它上次出现位置之间的距离d。若d大于f(i-1),即说明前一个非重复子字符串中没有包含当前字符,则可以添加当前字符到前一个非重复子字符串中,所以,f(i) = f(i-1)+1。若d小于或等于f(i-1),即说明前一个非重复子字符串中已经包含当前字符,则不可以添加当前字符,所以,f(i) = d。

关键点:动态规划,两个重复字符的距离

    private static int findLongestSubstringLength(String string)
    {
        if (string == null || string.equals("")) return 0;
        int maxLength = 0;
        int curLength = 0;
        int[] positions = new int[26];
        for (int i = 0; i < positions.length; i++)
        {
            positions[i] = -1; //初始化为-1,负数表示没出现过
        }
        for (int i = 0; i < string.length(); i++)
        {
            int curChar = string.charAt(i) - 'a';
            int prePosition = positions[curChar];
            //当前字符与它上次出现位置之间的距离
            int distance = i - prePosition;
            //当前字符第一次出现,或者前一个非重复子字符串中没有包含当前字符
            if (prePosition < 0 || distance > curLength){
                curLength++;
            } else
            {
                //更新最长非重复子字符串的长度
                if (curLength > maxLength)
                {
                    maxLength = curLength;
                }
                curLength = distance;
            }
            positions[curChar] = i; //更新字符出现的位置
        }
        if (curLength > maxLength)
        {
            maxLength = curLength;
        }
        return maxLength;
    }

丑数

题目描述
把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

public class Solution {
    public int GetUglyNumber_Solution(int index) {
        if(index<7)
            return index;
        int p2 = 0;
        int p3 = 0;
        int p5 = 0;
        int a[] = new int[index];
        a[0] = 1;
        for(int i=1;i<index;i++){
            a[i] = Math.min(a[p2]*2,Math.min(a[p3]*3,a[p5]*5));
            if(a[i] == a[p2]*2)p2++;
            if(a[i] == a[p3]*3)p3++;
            if(a[i] == a[p5]*5)p5++;
        }
        return a[index-1];
    }
}

52. 两个链表的第一个公共结点

解题思路
设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a

当访问链表 A 的指针访问到链表尾部时,令它从链表 B 的头部重新开始访问链表 B;同样地,当访问链表 B 的指针访问到链表尾部时,令它从链表 A 的头部重新开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。

public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        ListNode p1 = pHead1;
        ListNode p2 = pHead2;
        while(p1!=p2){
            p1 = (p1==null?pHead2:p1.next);
            p2 = (p2==null?pHead1:p2.next);
        }
        return p1; 
    }
}

57.2 和为 S 的连续正数序列

题目描述
输出所有和为 S 的连续正数序列。

例如和为 100 的连续序列有:

[9, 10, 11, 12, 13, 14, 15, 16]
[18, 19, 20, 21, 22]。

思路:双指针实现滑动窗口


import java.util.ArrayList;
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
        //存放结果
        ArrayList<ArrayList<Integer> > result = new ArrayList<>();
        //两个起点,相当于动态窗口的两边,根据其窗口内的值的和来确定窗口的位置和大小
        int plow = 1,phigh = 2;
        while(phigh > plow){
            //由于是连续的,差为1的一个序列,那么求和公式是(a0+an)*n/2
            int cur = (phigh + plow) * (phigh - plow + 1) / 2;
            //相等,那么就将窗口范围的所有数添加进结果集
            if(cur == sum){
                ArrayList<Integer> list = new ArrayList<>();
                for(int i=plow;i<=phigh;i++){
                    list.add(i);
                }
                result.add(list);
                plow++;
            //如果当前窗口内的值之和小于sum,那么右边窗口右移一下
            }else if(cur < sum){
                phigh++;
            }else{
            //如果当前窗口内的值之和大于sum,那么左边窗口右移一下
                plow++;
            }
        }
        return result;
    }
}

58.1 翻转单词顺序列

题目描述
Input:
“I am a student.”

Output:
“student. a am I”
Copy to clipboardErrorCopied
解题思路
题目应该有一个隐含条件,就是不能用额外的空间。虽然 Java 的题目输入参数为 String 类型,需要先创建一个字符数组使得空间复杂度为 O(N),但是正确的参数类型应该和原书一样,为字符数组,并且只能使用该字符数组的空间。任何使用了额外空间的解法在面试时都会大打折扣,包括递归解法。

正确的解法应该是和书上一样,先旋转每个单词,再旋转整个字符串。

public String ReverseSentence(String str) {
    int n = str.length();
    char[] chars = str.toCharArray();
    int i = 0, j = 0;
    while (j <= n) {
        if (j == n || chars[j] == ' ') {
            reverse(chars, i, j - 1);
            i = j + 1;
        }
        j++;
    }
    reverse(chars, 0, n - 1);
    return new String(chars);
}

private void reverse(char[] c, int i, int j) {
    while (i < j)
        swap(c, i++, j--);
}

private void swap(char[] c, int i, int j) {
    char t = c[i];
    c[i] = c[j];
    c[j] = t;
}

58.2 左旋转字符串

题目描述
Input:
S=“abcXYZdef”
K=3

Output:
“XYZdefabc”
Copy to clipboardErrorCopied
解题思路
先将 “abc” 和 “XYZdef” 分别翻转,得到 “cbafedZYX”,然后再把整个字符串翻转得到 “XYZdefabc”。

public String LeftRotateString(String str, int n) {
    if (n >= str.length())
        return str;
    char[] chars = str.toCharArray();
    reverse(chars, 0, n - 1);
    reverse(chars, n, chars.length - 1);
    reverse(chars, 0, chars.length - 1);
    return new String(chars);
}

private void reverse(char[] chars, int i, int j) {
    while (i < j)
        swap(chars, i++, j--);
}

private void swap(char[] chars, int i, int j) {
    char t = chars[i];
    chars[i] = chars[j];
    chars[j] = t;
}

59. 滑动窗口的最大值

NowCoder

题目描述
给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。

例如,如果输入数组 {2, 3, 4, 2, 6, 2, 5, 1} 及滑动窗口的大小 3,那么一共存在 6 个滑动窗口,他们的最大值分别为 {4, 4, 6, 6, 6, 5}。

解题思路

public ArrayList<Integer> maxInWindows(int[] num, int size) {
    ArrayList<Integer> ret = new ArrayList<>();
    if (size > num.length || size < 1)
        return ret;
    PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1);  /* 大顶堆 */
    for (int i = 0; i < size; i++)
        heap.add(num[i]);
    ret.add(heap.peek());
    for (int i = 0, j = i + size; j < num.length; i++, j++) {            /* 维护一个大小为 size 的大顶堆 */
        heap.remove(num[i]);
        heap.add(num[j]);
        ret.add(heap.peek());
    }
    return ret;
}

n 个骰子的点数(dp)

题目描述
把 n 个骰子扔在地上,求点数和为 s 的概率。

解题思路
动态规划
使用一个二维数组 dp 存储点数出现的次数,其中 dp[i][j] 表示前 i 个骰子产生点数 j 的次数。
动态规划
1.现在变量有:骰子个数,点数和。当有k个骰子,点数和为n时,出现次数记为f(k,n)。那与k-1个骰子阶段之间的关系是怎样的?

2.当有k-1个骰子时,再增加一个骰子,这个骰子的点数只可能为1、2、3、4、5或6。那k个骰子得到点数和为n的情况有:

(k-1,n-1):第k个骰子投了点数1

(k-1,n-2):第k个骰子投了点数2

(k-1,n-3):第k个骰子投了点数3

(k-1,n-6):第k个骰子投了点数6

在k-1个骰子的基础上,再增加一个骰子出现点数和为n的结果只有这6种情况!
所以:f(k,n)=f(k-1,n-1)+f(k-1,n-2)+f(k-1,n-3)+f(k-1,n-4)+f(k-1,n-5)+f(k-1,n-6)
3.有1个骰子,f(1,1)=f(1,2)=f(1,3)=f(1,4)=f(1,5)=f(1,6)=1。
递归函数,返回和为n出现的次数。所有的和出现次数总和为6^n。

空间复杂度:O(N2)

public List<Map.Entry<Integer, Double>> dicesSum(int n) {
    final int face = 6;
    final int pointNum = face * n;
    long[][] dp = new long[n + 1][pointNum + 1];

    for (int i = 1; i <= face; i++)
        dp[1][i] = 1;

    for (int i = 2; i <= n; i++)
        for (int j = i; j <= i*face; j++)     /* 使用 i 个骰子最小点数为 i */
            for (int k = 1; k <= face && k < j; k++)//k
                dp[i][j] += dp[i - 1][j - k];

    final double totalNum = Math.pow(6, n);
    List<Map.Entry<Integer, Double>> ret = new ArrayList<>();
    for (int i = n; i <= pointNum; i++)
        ret.add(new AbstractMap.SimpleEntry<>(i, dp[n][i] / totalNum));

    return ret;
}

63. 股票的最大利润

Leetcode

题目描述
可以有一次买入和一次卖出,买入必须在前。求最大收益。

假设给定的数组为:

[7, 1, 5, 3, 6, 4]

如果我们在图表上绘制给定数组中的数字,我们将会得到:

在这里插入图片描述

使我们感兴趣的点是上图中的峰和谷。我们需要找到最小的谷之后的最大的峰。
我们可以维持两个变量——minprice 和 maxprofit,它们分别对应迄今为止所得到的最小的谷值和最大的利润(卖出价格与最低价格之间的最大差值)

public class Solution {
    public int maxProfit(int prices[]) {
        int minprice = Integer.MAX_VALUE;
        int maxprofit = 0;
        for (int i = 0; i < prices.length; i++) {
            if (prices[i] < minprice)
                minprice = prices[i];
            else if (prices[i] - minprice > maxprofit)
                maxprofit = prices[i] - minprice;
        }
        return maxprofit;
    }
}

不用加减乘除做加法

题目描述
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

首先看十进制是如何做的: 5+7=12,三步走
第一步:相加各位的值,不算进位,得到2。
第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。

第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。

同样我们可以用三步走的方式计算二进制值相加: 5-101,7-111 第一步:相加各位的值,不算进位,得到010,二进制每位相加就相当于各位做异或操作,101^111。

第二步:计算进位值,得到1010,相当于各位做与操作得到101,再向左移一位得到1010,(101&111)<<1。

第三步重复上述两步, 各位相加 010^1010=1000,进位值为100=(010&1010)<<1。
     继续重复上述两步:1000^100 = 1100,进位值为0,跳出循环,1100为最终结果。
public class Solution {
    public int Add(int num1,int num2) {
        while (num2!=0) {
            int temp = num1^num2;
            num2 = (num1&num2)<<1;
            num1 = temp;
        }
        return num1;
    }
}

剑指OFFER_第4张图片

public int[] multiply(int[] A) {
    int n = A.length;
    int[] B = new int[n];
    for (int i = 0, product = 1; i < n; product *= A[i], i++)       /* 从左往右累乘 */
        B[i] = product;
    for (int i = n - 1, product = 1; i >= 0; product *= A[i], i--)  /* 从右往左累乘 */
        B[i] *= product;
    return B;
}

68. 树中两个节点的最低公共祖先

解题思路
二叉查找树:二叉查找树中,两个节点 p, q 的公共祖先 root 满足 root.val >= p.val && root.val <= q.val。

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    if (root == null)
        return root;
    if (root.val > p.val && root.val > q.val)
        return lowestCommonAncestor(root.left, p, q);
    if (root.val < p.val && root.val < q.val)
        return lowestCommonAncestor(root.right, p, q);
    return root;
}
lass Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {

        // Value of p
        int pVal = p.val;

        // Value of q;
        int qVal = q.val;

        // Start from the root node of the tree
        TreeNode node = root;

        // Traverse the tree
        while (node != null) {

            // Value of ancestor/parent node.
            int parentVal = node.val;

            if (pVal > parentVal && qVal > parentVal) {
                // If both p and q are greater than parent
                node = node.right;
            } else if (pVal < parentVal && qVal < parentVal) {
                // If both p and q are lesser than parent
                node = node.left;
            } else {
                // We have found the split point, i.e. the LCA node.
                return node;
            }
        }
        return null;
    }
}

普通二叉树
Leetcode : 236. Lowest Common Ancestor of a Binary Tree

在左右子树中查找是否存在 p 或者 q,如果 p 和 q 分别在两个子树中,那么就说明根节点就是最低公共祖先。

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root==null||root==p||root==q){
            return root;
        }
        TreeNode left = lowestCommonAncestor(root.left,p,q);
        TreeNode right = lowestCommonAncestor(root.right,p,q);
        return left==null?right:(right==null?left:root);//第二个括号为:如果左右子树均不为空,返回root;
        
    }
}

你可能感兴趣的:(剑指offer)