剑指offer之JAVA版---史上最全

整理电脑资料,又发现了剑指offer系列,想起来当时反复刷题,反复面试的自己,很忙碌,也很充实。剑指offer可以说是经典系列了,记得当时Java版资料零零散散,就自己整理了一份。希望能帮助到陌生的你。

1.单例模式

Ⅰ 懒汉式-线程不安全

以下实现中,私有静态变量 uniqueInstance 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。
这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (uniqueInstance == null) ,并且此时 uniqueInstance 为 null,那么会有多个线程执行 uniqueInstance = new Singleton(); 语句,这将导致实例化多次 uniqueInstance。

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

Ⅱ 饿汉式-线程安全

线程不安全问题主要是由于 uniqueInstance 被实例化多次,采取直接实例化 uniqueInstance 的方式就不会产生线程不安全问题。

但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。

public class Singleton {

    private Singleton() {
    }
    private static Singleton uniqueInstance = new Singleton();

    public static Singleton getUniqueInstance() {
       
        return uniqueInstance;
    }
}

Ⅲ 懒汉式-线程安全

只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance。

但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 uniqueInstance 已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。

public static synchronized Singleton getUniqueInstance() {
    if (uniqueInstance == null) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}

Ⅳ 双重校验锁-线程安全

uniqueInstance 只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 uniqueInstance 没有被实例化时,才需要进行加锁。

双重校验锁先判断 uniqueInstance 是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            Synchronized(Singleton.class){
               if (uniqueInstance == null){
                   uniqueInstance = new Singleton();
                }
             }
        }
        return uniqueInstance;
    }
}

值得注意的是,上面使用了两个if (uniqueInstance == null)语句判断,因为如果两个线程都通过了第一个if语句,那么两个线程都会进入if语句块内,即使if语句块内有加锁操作,但是两个线程都会执行new 语句,所以需要两个if (uniqueInstance == null)语句判断,即为双重校验锁。

Ⅴ 静态内部类实现

当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance() 方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。

这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。

public class Singleton {

   private Singleton() {
   }

   private static class SingletonHolder {
       private static final Singleton INSTANCE = new Singleton();
   }

   public static Singleton getUniqueInstance() {
       return SingletonHolder.INSTANCE;
   }
}

2.数组中重复的数字

题目描述:
在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
Input:
{2, 3, 1, 0, 2, 5}

Output:
2

思路分析:
这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素调整到第 i 个位置上。如果这个数组中没有重复的数字,那么当数组排序后数字i将出现在下标为i的位置。由于数组中有重复的数字,有些位置可能存在多个数字,同时有些位置可能没有数字。

以 (2, 3, 1, 0, 2, 5) 为例:
数组的第0个数字(从0开始计数,和数组的下标保持一致)是2,与它的下标不相等,于是把它和下标为2的数字1交换,交换后的数组是{1,3,2,0,2,5}。此时第0 个数字是1,仍然与它的下标不相等,继续把它和下标为1的数字3交换,得到数组{0,1,2,3,2,5,3}。此时第0 个数字为0,接着扫描下一个数字,在接下来的几个数字中,下标为1,2,3的三个数字分别为1,2,3,他们的下标和数值都分别相等,因此不需要做任何操作。接下来扫描下标为4的数字2.由于它的值与它的下标不相等,再比较它和下标为2的数字。注意到此时数组中下标为2的数字也是2,也就是数字2和下标为2和下标4的两个位置都出现了,因此找到一个重复的数字。

position-0 : (2,3,1,0,2,5) // 2 <-> 1
             (1,3,2,0,2,5) // 1 <-> 3
             (3,1,2,0,2,5) // 3 <-> 0
             (0,1,2,3,2,5) // already in position
position-1 : (0,1,2,3,2,5) // already in position
position-2 : (0,1,2,3,2,5) // already in position
position-3 : (0,1,2,3,2,5) // already in position
position-4 : (0,1,2,3,2,5) // nums[i] == nums[nums[i]], exit

代码实现:

public boolean duplicate(int[] nums, int length, int[] duplication) {
    if (nums == null || length <= 0)
        return false;
    for (int i = 0; i < length; i++) {
        while (nums[i] != i) {
            if (nums[i] == nums[nums[i]]) {
                duplication[0] = nums[i];
                return true;
            }
            swap(nums, i, nums[i]);
        }
    }
    return false;
}
private void swap(int[] nums, int i, int j) {
    int t = nums[i];
    nums[i] = nums[j];
    nums[j] = t;
}

3.二维数组中的查找

题目描述:

在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

Consider the following matrix:
[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]

Given target = 5, return true.
Given target = 20, return false.

解题思路:

从右上角开始查找。矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。
从右上角开始比较,目标数大于右上角的数,那么第一行可以去掉,行+1;小于右上角的数,那么最后一列可以去掉,列-1,一直重复动作即可。

public boolean Find(int target, int[][] matrix) {
    if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
        return false;
    int rows = matrix.length, cols = matrix[0].length;
    int r = 0, c = cols - 1; // 从右上角开始
    while (r <= rows - 1 && c >= 0) {
        if (target == matrix[r][c])
            return true;
        else if (target > matrix[r][c])
            r++;
        else
            c--;
    }
    return false;
}

4.替换空格

题目描述:
将一个字符串中的空格替换成 “%20”。
Input:
“We Are Happy”

Output:
“We%20Are%20Happy”

思路分析:
在字符串尾部填充任意字符,使得字符串的长度等于替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。
如果遍历到一个空格将空格后字符串后移两个字符空间的话,会影响执行效率,所以可以先将字符串的空格个数得到,在末尾添加足够的字符空间。

令 P1 指向字符串原来的末尾位置,P2 指向扩展后字符串的末尾位置。P1 和 P2 从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。直到P1和P2指向同一个位置时完成替换。

代码实现:

public String replaceSpace(StringBuffer str) {
    int P1 = str.length() - 1;
    for (int i = 0; i <= P1; i++)
        if (str.charAt(i) == ' ')
            str.append("  ");

    int P2 = str.length() - 1;
while (P1 >= 0 && P2 > P1) {
   //这里P1--先用再减,不用担心最后一个字符
        char c = str.charAt(P1--);
        if (c == ' ') {
            str.setCharAt(P2--, '0');
            str.setCharAt(P2--, '2');
            str.setCharAt(P2--, '%');
        } else {
            str.setCharAt(P2--, c);
        }
    }
    return str.toString();
}

5.从尾到头打印链表

题目描述:
输入链表的第一个节点,从尾到头反过来打印出每个结点的值。
剑指offer之JAVA版---史上最全_第1张图片

方法一:使用栈

public ArrayList printListFromTailToHead(ListNode listNode) {
    Stack stack = new Stack<>();
    while (listNode != null) {
        stack.add(listNode.val);
        listNode = listNode.next;
    }
    ArrayList ret = new ArrayList<>();
    while (!stack.isEmpty())
        ret.add(stack.pop());
    return ret;
} 

方法二:使用递归

public ArrayList printListFromTailToHead(ListNode listNode) {
    ArrayList ret = new ArrayList<>();
    if (listNode != null) {
        ret.addAll(printListFromTailToHead(listNode.next));
        ret.add(listNode.val);
    }
    return ret;
}

方法三:使用头插法

头插法:就是反复让当前新生成的结点指向头结点的next,然后头结点的next有反过来指向当前生成的新结点。反复如此头插法就像是插队一样,即每次新结点都是插到第一个结点的位置,有倒序的效果。

public ArrayList printListFromTailToHead(ListNode listNode) {
    // 头插法构建逆序链表
    ListNode head = new ListNode(-1);
while (listNode != null) {
    //先将listNode链表的下一个值保存,以免被覆盖
        ListNode memo = listNode.next;
        //头插法
        listNode.next = head.next;
        head.next = listNode;
        //listNode.next作为下一个循环的listNode
        listNode = memo;
    }
    // 构建 ArrayList
    ArrayList ret = new ArrayList<>();
    head = head.next;
    while (head != null) {
        ret.add(head.val);
        head = head.next;
    }
    return ret;
}

方法四:使用Collections.reverse()

public ArrayList printListFromTailToHead(ListNode listNode) {
    ArrayList ret = new ArrayList<>();
    while (listNode != null) {
        ret.add(listNode.val);
        listNode = listNode.next;
    }
    Collections.reverse(ret);
    return ret;
}

6.重建二叉树 (*)

题目描述:
根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
preorder = [3,9,20,15,7]
inorder = [9,3,15,20,7]

思路分析:
前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。
既然我们已经分别找到了左、右子树的前序遍历序列和中序遍历序列,我们可以用同样的方法分别去构建左右子树。也就是说,接下来的事情可以用递归的方法去完成。

// 缓存中序遍历数组每个值对应的索引、
private Map indexForInOrders = new HashMap<>();

public TreeNode reConstructBinaryTree(int[] pre, int[] in) {
for (int i = 0; i < in.length; i++)
    //将中序遍历放进有索引的数组,方便获得根节点的索引
        indexForInOrders.put(in[i], i);
    return reConstructBinaryTree(pre, 0, pre.length - 1, 0);
}
/*  preL:子树的左边界在前序遍历数组的索引
    preR:子树的右边界在前序遍历数组的索引
    inL:子树的左边界在中序遍历数组的索引
  */
private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL) {
    
    if (preL > preR)
        return null;
    //前序遍历数组的第一个值是根节点
TreeNode root = new TreeNode(pre[preL]);
//根据root的值去查询root在中序遍历数组中的位置索引
int inIndex = indexForInOrders.get(root.val);
//得到左子树的大小
int leftTreeSize = inIndex - inL;
//继续用这种办法还原左子树和右子树,递归调用
    root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL);
    root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1);
    return root;
}

7.二叉树的下一个节点

题目描述:
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。

public class TreeLinkNode {

    int val;
    TreeLinkNode left = null;
    TreeLinkNode right = null;
    TreeLinkNode next = null;

    TreeLinkNode(int val) {
        this.val = val;
    }
}

解题思路:
① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点;
剑指offer之JAVA版---史上最全_第2张图片

② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。
剑指offer之JAVA版---史上最全_第3张图片

public TreeLinkNode GetNext(TreeLinkNode pNode) {
    if (pNode.right != null) {
        TreeLinkNode node = pNode.right;
        //注意是while循环,若左子树一直存在那么一直找到叶节点
        while (node.left != null)
            node = node.left;
        return node;
    } else {
        while (pNode.next != null) {
            TreeLinkNode parent = pNode.next;
            //找到所在左子树的父节点
            if (parent.left == pNode)
                return parent;
            pNode = pNode.next;
        }
    }
    return null;
}

8.用栈实现队列

题目描述:
用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。
思路分析:
in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。
剑指offer之JAVA版---史上最全_第4张图片

Stack in = new Stack();
Stack out = new Stack();
public void push(int node) {
    in.push(node);
}
public int pop() throws Exception {
    if (out.isEmpty())
        while (!in.isEmpty())
            out.push(in.pop());

    if (out.isEmpty())
        throw new Exception("queue is empty");

    return out.pop();
}

9.1斐波那契数列

题目描述:
求斐波那契数列的第 n 项,n <= 39。
在这里插入图片描述

思路分析:
利用递归实现。递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。

public int Fibonacci(int n) {
    if (n <= 1)
        return n;
    int[] fib = new int[n + 1];
    fib[1] = 1;
    for (int i = 2; i <= n; i++)
        fib[i] = fib[i - 1] + fib[i - 2];
    return fib[n];
}

考虑到第 i 项只与第 i-1 和第 i-2 项有关,因此只需要存储前两项的值就能求解第 i 项,从而将空间复杂度由 O(N) 降低为 O(1)。

public int Fibonacci(int n) {
    if (n <= 1)
        return n;
    int pre2 = 0, pre1 = 1;
    int fib = 0;
    for (int i = 2; i <= n; i++) {
        fib = pre2 + pre1;
        pre2 = pre1;
        pre1 = fib;
    }
    return fib;
}

由于待求解的 n 小于 40,因此可以将前 40 项的结果先进行计算,之后就能以 O(1) 时间复杂度得到第 n 项的值了。

public class Solution {

    private int[] fib = new int[40];

    public Solution() {
        fib[1] = 1;
        fib[2] = 2;
        for (int i = 2; i < fib.length; i++)
            fib[i] = fib[i - 1] + fib[i - 2];
    }

    public int Fibonacci(int n) {
        return fib[n];
    }
}

9.2跳台阶

题目描述:
一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
思路分析:
假定第一次跳的是一阶,那么剩下的是n-1个台阶,跳法是f(n-1);假定第一次跳的是2阶,那么剩下的是n-2个台阶,跳法是f(n-2)。所以得到总跳法:f(n) = f(n-1)+f(n-2)。特别的,n=1时, f(n)=1 ; n=2时, f(n)=2。
实际上是一个斐波那契数列。也就是求斐波那契数列的第n项值。

public int JumpFloor(int n) {
    if (n <= 2)
        return n;
    int pre2 = 1, pre1 = 2;
    int result = 1;
    for (int i = 2; i < n; i++) {
        result = pre2 + pre1;
        pre2 = pre1;
        pre1 = result;
    }
    return result;
}

9.3变态跳台阶

题目描述:
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
思路分析:
跟上面的跳台阶相同的思路,可以得到以下推理:
f(n) = f(n-1) + f(n-2) + f(n-3) + …+ f(n-(n-1))+1
f(n-1) = f(n-2) + f(n-3) + f(n-4) + …+ f(n-(n-1))+1
根据上面的分析可以推理得到: f(n) = 2* f(n-1)。特别的,当n=1时,f(n) = 1;当n=2时,f(n) = 2.

public class Solution {
    public int JumpFloorII(int target) {
        if (target <= 0) {
            return -1;
        } else if (target == 1) {
            return 1;
        } else {
            return 2 * JumpFloorII(target - 1);
        }
    }
}

当然还有一种解法是:每个台阶都有跳与不跳两种情况(除了最后一个台阶),最后一个台阶必须跳。所以共用2^(n-1)种情况。

9.4 矩形覆盖

题目描述:
我们可以用 21 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 21 的小矩形无重叠地覆盖一个 2n 的大矩形,总共有多少种方法?
思路分析:
依旧是斐波那契数列,
① target = 1大矩形为2
1,只有一种摆放方法,return1;
② target = 2 大矩形为22,有两种摆放方法,return2;
③ target = n 分为两步考虑:
第一次竖着摆放一块 2
1 的小矩阵,则摆放方法总共为f(target - 1);
第一次横着摆放一块2*1 的小矩阵,则摆放方法总共为f(target - 2)。

10.旋转数组的最小数字(*)

题目描述:
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。

例如数组 {3, 4, 5, 1, 2} 为 {1, 2, 3, 4, 5} 的一个旋转,该数组的最小值为 1。
解题思路:
在一个有序数组中查找一个元素可以用二分查找,二分查找也称为折半查找,每次都能将查找区间减半,这种折半特性的算法时间复杂度都为 O(logN)。

本题可以修改二分查找算法进行求解:
当 nums[m] <= nums[h] 的情况下,说明解在 [l, m] 之间,此时令 h = m;否则解在 [m + 1, h] 之间,令 l = m + 1。

public int minNumberInRotateArray(int[] nums) {
    if (nums.length == 0)
        return 0;
    int l = 0, h = nums.length - 1;
    while (l < h) {
        int m = l + (h - l) / 2;
        if (nums[m] <= nums[h])
            h = m;
        else
            l = m + 1;
    }
    return nums[l];
}
如果数组元素允许重复的话,那么就会出现一个特殊的情况:nums[l] == nums[m] == nums[h],那么此时无法确定解在哪个区间,需要切换到顺序查找。
例如对于数组 {1,1,1,0,1},l、m 和 h 指向的数都为 1,此时无法知道最小数字 0 在哪个区间。
public int minNumberInRotateArray(int[] nums) {
    if (nums.length == 0)
        return 0;
    int l = 0, h = nums.length - 1;
    while (l < h) {
        int m = l + (h - l) / 2;
        if (nums[l] == nums[m] && nums[m] == nums[h])
            return minNumber(nums, l, h);
        else if (nums[m] <= nums[h])
            h = m;
        else
            l = m + 1;
    }
    return nums[l];
}
private int minNumber(int[] nums, int l, int h) {
for (int i = l; i < h; i++)
    //????????
        if (nums[i] > nums[i + 1])
            return nums[i + 1];
    return nums[l];
}

11.矩阵中的路径(*)

题目描述:
请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。

例如下面的矩阵包含了一条 bfce 路径。
剑指offer之JAVA版---史上最全_第5张图片

思路分析:
首先,在矩阵中任选一个格子作为路径的起点。当矩阵中坐标为(row,col)的格子和路径字符串中相应的字符一样时,从4个相邻的格子(row,col-1),(row-1,col),(row,col+1)以及(row+1,col)中去定位路径字符串中下一个字符。
除在矩阵边界上的格子之外,其他格子都有4个相邻的格子。
如果4个相邻的格子都没有匹配字符串中下一个的字符,表明当前路径字符串中字符在矩阵中的定位不正确,我们需要回到前一个,然后重新定位。一直重复这个过程,直到路径字符串上所有字符都在矩阵中找到合适的位置。
由于矩阵的格子不能二次进入,还需要定义和字符矩阵大小一样的布尔值矩阵,用来标识路径是否已经进入每个格子。

private final static int[][] next = {
    {0, -1}, {0, 1}, {-1, 0}, {1, 0}};
private int rows;
private int cols;

public boolean hasPath(char[] array, int rows, int cols, char[] str) {
    if (rows == 0 || cols == 0)
        return false;
    this.rows = rows;
this.cols = cols;

//创建标记矩阵
boolean[][] marked = new boolean[rows][cols];
//创建要操作的二维矩阵
char[][] matrix = buildMatrix(array);

    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            if (backtracking(matrix, str, marked, 0, i, j))
                return true;
    return false;
}

/*  回溯算法 :
     matrix:要操作的二维矩阵
         str:目标字符串
      marked:标记矩阵
pathLen:当前路径长度
      r:当前处理的行
      c:当前处理的列
  */
private boolean backtracking(char[][] matrix, char[] str, boolean[][] marked, 
int pathLen,int r, int c) {
    if (pathLen == str.length)
        return true;
    if (r < 0 || r >= rows || c < 0 || c >= cols || matrix[r][c] != str[pathLen] || marked[r][c])
        return false;
marked[r][c] = true;
//对行、列操作实现判断上下左右四个相邻的值,如next中第一行{0, -1},表示当前行r不变,列c-1
    for (int[] n : next)
        if (backtracking(matrix, str, marked, pathLen + 1, r + n[0], c + n[1]))
            return true;
    marked[r][c] = false;
    return false;
}

/*  创建矩阵:将传进来的一维数组转换成二维矩阵  */
private char[][] buildMatrix(char[] array) {
    char[][] matrix = new char[rows][cols];
    for (int i = 0, idx = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            matrix[i][j] = array[idx++];
    return matrix;
}

12.机器人的运动范围

题目描述:
地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。
例如,当 k 为 18 时,机器人能够进入方格 (35,37),因为 3+5+3+7=18。但是,它不能进入方格 (35,38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子?

思路分析:
与上题类似,这个方格也可以看成一个m*n的矩阵。同样在这个矩阵中,除边界上的格子之外其他格子都有四个相邻的格子。机器人从坐标(0,0)开始移动。当它准备进入坐标为(i,j)的格子时,通过检查坐标的数位和来判断机器人是否能够进入。如果机器人能够进入坐标为(i,j)的格子,我们接着再判断它能否进入四个相邻的格子(i,j-1)、(i-1,j),(i,j+1)和(i+1,j)。

private static final int[][] next = {
    {0, -1}, {0, 1}, {-1, 0}, {1, 0}};
private int cnt = 0;   //可进入的格子的个数
private int rows;
private int cols;
private int threshold;   //坐标数之和的上限
private int[][] digitSum;  //坐标数和的矩阵

public int movingCount(int threshold, int rows, int cols) {
    this.rows = rows;
    this.cols = cols;
    this.threshold = threshold;
    initDigitSum();
    boolean[][] marked = new boolean[rows][cols];
    dfs(marked, 0, 0);
    return cnt;
}

/*  计算满足条件的格子数  */
private void dfs(boolean[][] marked, int r, int c) {
    if (r < 0 || r >= rows || c < 0 || c >= cols || marked[r][c])
        return;
    marked[r][c] = true;
    if (this.digitSum[r][c] > this.threshold)
        return;
    cnt++;
    for (int[] n : next)
        dfs(marked, r + n[0], c + n[1]);
}

/*   将矩阵坐标数之和先算出来   */
private void initDigitSum() {
    //取行、列的较大值计算十位个位之和
    int[] digitSumOne = new int[Math.max(rows, cols)];
    for (int i = 0; i < digitSumOne.length; i++) {
        int n = i;
        while (n > 0) {
            digitSumOne[i] += n % 10;
            n /= 10;
        }
    }
    this.digitSum = new int[rows][cols];
    for (int i = 0; i < this.rows; i++)
        for (int j = 0; j < this.cols; j++)
            //计算行数和列数加起来的和
            this.digitSum[i][j] = digitSumOne[i] + digitSumOne[j];
}

13.剪绳子

题目描述:
把一根绳子剪成多段,并且使得每段的长度乘积最大。
n = 2
return 1 (2 = 1 + 1)

n = 10
return 36 (10 = 3 + 3 + 4)

问题分析:

方法一:动态规划
设f(n)代表长度为n的绳子剪成若干段的最大乘积,如果第一刀下去,第一段长度是i,那么剩下的就需要剪n-i,那么f(n)=max{f(i)f(n-i)}。所以f(n)的最优解对应着f(i)和f(n-i)的最优解。
f(n)=max{f(1)f(n-1), f(2)f(n-2), f(3)f(n-3), …, f(i)(fn-i), …}
因为需要保证f(i)f(n-i)不重复,就需要保证i<=n/2,这是一个限制条件,求1~n/2范围内的乘积,得到最大值。
剪绳子是最优解问题,其次,大问题包含小问题,并且大问题的最优解包含着小问题的最优解,所以可以使用动态规划求解问题,并且从小到大(2–>n)求解,把小问题的最优解记录在数组中,求大问题最优解时就可以直接获取,避免重复计算。

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

//i代表长度为2,3...,n的绳子
    for (int i = 2; i <= n; i++)
        for (int j = 1; j <= i/2; j++)
           //计算绳长比较大的最优解dp[i]时会用到绳长较小的最优解dp[j]
           //里面的取最大值Math.max(j * (i - j), dp[j] * (i - j))是比较“j整段”乘积,和“j分段最优解”乘积哪个大
           //外面的取最大值是判断j取多大时有最优解
            dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i-j]));
    return dp[n];
}

方法二:贪心算法
1)贪心算法在对问题求解时,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解;
2)选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关;
3)题目贪心策略:当n>=3时,尽可能多地剪长度为3的绳子;当对3整除余1时,凑成一个4,把绳子剪成两段长度为2的绳子;当对3整除余2时,把2作为一段。

public int integerBreak(int n) {
    if (n < 2)
        return 0;
    if (n == 2)
        return 1;
    if (n == 3)
        return 2;
//整除3,timesOf3代表最多能截取多少段3
int timesOf3 = n / 3;
//整除3余1时,凑成一个4,截成两个2
    if (n - timesOf3 * 3 == 1)
        timesOf3--;
int timesOf2 = (n - timesOf3 * 3) / 2;
//Math.pow(a, b)表示求a的b次幂
    return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2));
}

14.二进制中1的个数

题目描述:
输入一个整数,输出该数二进制表示中 1 的个数。
思路分析:
利用“n&(n-1)”,该位运算执行一次,去掉二进制中一个1,直到远算结果为0时去掉所有的1。

n       : 10110100
n-1     : 10110011
n&(n-1) : 10110000
//cnt:二进制中1的个数
public int NumberOf1(int n) {
    int cnt = 0;
    while (n != 0) {
        cnt++;
        n &= (n - 1);
    }
    return cnt;
}

还有一个常规解法,将n和flag做与运算,flag由1每次左移一位。

public static int fun(int num){
        int count = 0;
        int flag = 1;
        while(flag != 0){
            if((flag & num) != 0){
                count++;
            }
            flag = flag << 1;
        }
        return count;
    }
利用Integer.bitCount()方法
public int NumberOf1(int n) {
    return Integer.bitCount(n);
}

15.数值的整数次方

题目描述:
给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent,求 base 的 exponent 次方。
思路分析:
但我们可以换一种思路考虑:我们的目标是求出一个数字的32次方,如果我们已经知道了它的16次方,那么只要16次放的基础上再平方一次就可以了。而16次方又是8次方的平方。这样以此类推,我们求32次方只需要5次乘方:先求平方,在平方的基础上求4次方,在4次方的基础上求8次方,在8次方的基础上求16次方,最后在16此方的基础上求32次方。
在这里插入图片描述

因为 (x*x)n/2 可以通过递归求解,并且每次递归 n 都减小一半,因此整个算法的时间复杂度为 O(logN)。

public double Power(double base, int exponent) {
    if (exponent == 0)
        return 1;
    if (exponent == 1)
        return base;
    boolean isNegative = false;
    if (exponent < 0) {
        exponent = -exponent;
        isNegative = true;
    }
    double pow = Power(base * base, exponent / 2);
    if (exponent % 2 != 0)
        pow = pow * base;
    return isNegative ? 1 / pow : pow;
}

16.打印1到最大的n位数(*)

题目描述:
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。
思路分析:
实际上就是n位的对0-9的数字的全排列。

public void print1ToMaxOfNDigits(int n) {
    if (n <= 0)
        return;
    //char型数组number用来存放n位十进制数
    char[] number = new char[n];
    print1ToMaxOfNDigits(number, 0);
}


private void print1ToMaxOfNDigits(char[] number, int digit) {
    //digit表示number每一位对应的index
    if (digit == number.length) {
        printNumber(number);
        return;
}
//为什么要i + '0'????自动转换成字符类型,后面可以将‘0’过滤掉
    for (int i = 0; i < 10; i++) {
        number[digit] = (char) (i + '0');
        print1ToMaxOfNDigits(number, digit + 1);
    }
}

// 打印数字
private void printNumber(char[] number) {
int index = 0;
//有效数字前面的0不打印
    while (index < number.length && number[index] == '0')
        index++;
    while (index < number.length)
        System.out.print(number[index++]);
    System.out.println();
}

17.1 在O(1)时间内删除链表节点

思路分析:
① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 O(1)。
在这里插入图片描述

② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。
在这里插入图片描述

综上,如果进行 N 次操作,那么大约需要操作节点的次数为 N-1+N=2N-1,其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数,N 表示 1 个尾节点以 O(N) 的时间复杂度操作节点的总次数。(2N-1)/N ~ 2,因此该算法的平均时间复杂度为 O(1)。

public ListNode deleteNode(ListNode head, ListNode tobeDelete) {
    if (head == null || tobeDelete == null)
        return null;
    if (tobeDelete.next != null) {
        // 要删除的节点不是尾节点
        ListNode next = tobeDelete.next;
        tobeDelete.val = next.val;
        tobeDelete.next = next.next;
    } else {
        ListNode cur = head;
        while (cur.next != tobeDelete)
            cur = cur.next;
        cur.next = null;
    }
    return head;
}

17.2 删除排序链表中重复的节点

题目描述:
在这里插入图片描述

思路分析:

public ListNode deleteDuplication(ListNode pHead) {
    //链表为空或只有一个头节点直接返回
    if (pHead == null || pHead.next == null)
        return pHead;
    ListNode next = pHead.next;
if (pHead.val == next.val) {
    // 当前结点是重复结点
        while (next != null && pHead.val == next.val)
            //跳过值与当前结点相同的全部结点,找到第一个与当前结点不同的结点
            next = next.next;
        return deleteDuplication(next);
} else {    // 当前结点不是重复结点
    //保留当前结点,从下一个结点开始递归
        pHead.next = deleteDuplication(pHead.next);
        return pHead;
    }
}

18. 正则表达式匹配(*)

题目描述:
请实现一个函数用来匹配包括 ‘.’ 和 ‘’ 的正则表达式。模式中的字符 ‘.’ 表示任意一个字符,而 '’ 表示它前面的字符可以出现任意次(包含 0 次)。
在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 “aaa” 与模式 “a.a” 和 “abaca” 匹配,但是与 “aa.a” 和 “ab*a” 均不匹配。

思路分析:
(1)当模式中的第二个字符不是“*”时:
① 如果字符串第一个字符和模式中的第一个字符相匹配,那么字符串和模式都后移一个字符,然后匹配剩余的。
② 如果 字符串第一个字符和模式中的第一个字符相不匹配,直接返回false。

(2)而当模式中的第二个字符是“”时:
如果字符串第一个字符跟模式第一个字符不匹配,则模式后移2个字符,继续匹配。如果字符串第一个字符跟模式第一个字符匹配,可以有3种匹配方式:
③ 模式后移2字符,相当于x
被忽略;
④ 字符串后移1字符,模式后移2字符;
⑤ 字符串后移1字符,模式不变,即继续匹配字符下一位,因为*可以匹配多位;

这里需要注意的是:Java里,要时刻检验数组是否越界。

/**
 * 正则表达式匹配
 */
public class RegularMatch {
    /**
     * @param str 字符串
     * @param pattern 模式
     */
    public boolean match(char[] str,char[] pattern){
        //参数校验
        if(str == null || pattern == null || str.length == 0 || pattern.length == 0){
            return false;
        }
        return matchCore(str, 0, pattern, 0);
    }

    public boolean matchCore(char[] str, int strIndex, char[] pattern, int pIndex){
        //字符串和模式都已操作完,返回true
        if(strIndex >= str.length && pIndex >= pattern.length)
            return true;
        //字符串没有操作完,模式操作完,返回false
        if(strIndex < str.length && pIndex >= pattern.length)
            return false;
        //字符串操作完,模式没有操作完
        if(strIndex >= str.length && pIndex < pattern.length){
            if(pIndex + 1 < pattern.length && pattern[pIndex + 1] == '*')
                return matchCore(str, strIndex, pattern, pIndex+2);
            else
                return false;
        }
        /**
         * 字符串没有操作完,模式没有操作完
         */
        //如果模式的下一个字符为*
        if(pIndex + 1 < pattern.length && pattern[pIndex+1] == '*'){
            //字符串和模式的当前字符能够匹配
            if(str[strIndex] == pattern[pIndex]){
                return  matchCore(str, strIndex, pattern, pIndex+2)    //a*出现0次,strIndex不动,pIndex后移两位
                        ||matchCore(str, strIndex+1, pattern, pIndex+2)   //a*出现1次,strIndex后移一位,pIndex后移两位
                        ||matchCore(str, strIndex+1, pattern, pIndex);  //a*出现多次,strIndex后移,pIndex不变
            }else
                return matchCore(str, strIndex, pattern, pIndex+2);   //字符串和模式不匹配时,模式后移两位
        }else{
           //字符串当前字符匹配或模式当前为“.”
            if(str[strIndex] == pattern[pIndex] || pattern[pIndex] == '.'){
                return matchCore(str, strIndex+1, pattern, pIndex+1);
            }else
                return false;
        }
    }

还有一种解法,没看明白:(????)

public boolean match(char[] str, char[] pattern) {

    int m = str.length, n = pattern.length;
    boolean[][] dp = new boolean[m + 1][n + 1];

    dp[0][0] = true;
    for (int i = 1; i <= n; i++)
        if (pattern[i - 1] == '*')
            dp[0][i] = dp[0][i - 2];

    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= n; j++)
            if (str[i - 1] == pattern[j - 1] || pattern[j - 1] == '.')
                dp[i][j] = dp[i - 1][j - 1];
            else if (pattern[j - 1] == '*')
                if (pattern[j - 2] == str[i - 1] || pattern[j - 2] == '.') {
                    dp[i][j] |= dp[i][j - 1]; // a* counts as single a
                    dp[i][j] |= dp[i - 1][j]; // a* counts as multiple a
                    dp[i][j] |= dp[i][j - 2]; // a* counts as empty
                } else
                    dp[i][j] = dp[i][j - 2];   // a* only counts as empty

    return dp[m][n];
}

19.表示数值的字符串

题目描述:
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串”+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+)?");
}

20.调整数组顺序使奇数位于偶数前面

题目描述:
需要保证奇数和奇数,偶数和偶数之间的相对位置不变。
思路分析:

public void reOrderArray(int[] nums) {
    // 奇数个数
    int oddCnt = 0;
    for (int val : nums)
        if (val % 2 == 1)
            oddCnt++;
    int[] copy = nums.clone();
    int i = 0, j = oddCnt;
    for (int num : copy) {
        if (num % 2 == 1)
            nums[i++] = num;
        else
            nums[j++] = num;
    }
}

我们可以维护两个指针,第一个指针初始化时指向数组的第一个数字,它只向后移动;第二个指针初始化时指向数组的最后一个数字,它指向前移动。在两个指针相遇之前,第一个指针总是位于第二个指针的前面。如果第一个指针的数字是偶数,并且第二个指针指向的数字是奇数,我们就交换两个数字。

/**
 * 调整数组顺序使奇数位于偶数前面
 */
public class OddEventArray {

    public void reorderOddEvent(int[] array){
        if(array == null || array.length == 0){
            return ;
        }

        int len = array.length;
        int low = 0;
        int high = len - 1;
        while(low <= high){
            while(!isEvent(array[low]) && low <= len - 1){
                low++;
            }
            while(isEvent(array[high]) && high >= 0){
                high--;
            }
            if(low <= high){
                int temp = array[low];
                array[low] = array[high];
                array[high] = temp;
            }
        }
    }

    //判断是否是偶数,true:偶数;false:奇数
    public boolean isEvent(int num){
        return (num & 1) == 0;
    }

21.链表中倒数第k个结点

题目描述:
输入一个链表,输出该链表中倒数第k个结点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾结点是倒数第1个结点。例如一个链表有6个结点,从头结点开始它们的值依次是1,2,3,4,5,6。这个链表的倒数第3个结点是值为4的结点。
解题思路:
设链表的长度为 N。设两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到 N - K 个节点处,该位置就是倒数第 K 个节点。
剑指offer之JAVA版---史上最全_第6张图片

public ListNode FindKthToTail(ListNode head, int k) {
    if (head == null)
        return null;
    ListNode P1 = head;
    while (P1 != null && k-- > 0)
        P1 = P1.next;
    if (k > 0)
        return null;
    ListNode P2 = head;
    while (P1 != null) {
        P1 = P1.next;
        P2 = P2.next;
    }
    return P2;
}

22.链表中环的入口结点

题目描述:
一个链表中包含环,请找出该链表的环的入口结点。要求不能使用额外的空间。
思路分析:
第一步,找环中相汇点。分别用p1,p2指向链表头部,p1每次走一步,p2每次走二步,直到p1p2找到在环中的相汇点。
第二步,找环的入口。接上步,当p1
p2时,p2所经过节点数为2x,p1所经过节点数为x,设环中有n个节点,p2比p1多走一圈有2x=n+x; n=x;可以看出p1实际走了一个环的步数,再让p2指向链表头部,p1位置不变,p1,p2每次走一步直到p1==p2; 此时p1指向环的入口。

剑指offer之JAVA版---史上最全_第7张图片

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;
}

23.反转链表

题目描述:
定义一个函数,输入一个链表的头结点,反转该链表并输出反转后的链表的头结点。链表结点如下:

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

ListNode(int val) { 
this.val = val; 
} 
}

思路分析:
① 递归实现

public ListNode ReverseList(ListNode head) {
    if (head == null || head.next == null)
        return head;
    ListNode next = head.next;
    head.next = null;
    ListNode newHead = ReverseList(next);
    next.next = head;
    return newHead;
}

② 迭代实现

public ListNode ReverseList(ListNode head) {
    if (head == null || head.next == null)
        return head;
    ListNode newList = new ListNode();   //新定义一个链表,保存反转后的头结点
    while (head != null) {
        ListNode next = head.next;  //记录当前结点的下一个结点
        head.next = newList;   //当前结点的next指向当前节点的前一个结点
         //当前节点的前一个结点和当前节点都后移一个结点
        newList = head;
        head = next;
    }
    return newList;
}

24.合并两个排序的链表

题目描述:
输入两个递增排序的链表,合并这两个链表并使新链表中的结点仍然是按照递增排序的。
在这里插入图片描述

思路分析:
首先分析合并两个链表的过程。首先是链表的头结点,链表1的头结点的值小于链表2的头结点的值,因此链表1的头结点将是合并后链表的头结点。后面的结点同理。
剑指offer之JAVA版---史上最全_第8张图片

① 递归实现

public ListNode Merge(ListNode list1, ListNode list2) {
    if (list1 == null)
        return list2;
    if (list2 == null)
        return list1;
    if (list1.val <= list2.val) {
        list1.next = Merge(list1.next, list2);
        return list1;
    } else {
        list2.next = Merge(list1, list2.next);
        return list2;
    }
}

② 迭代实现

public ListNode Merge(ListNode list1, ListNode list2) {
    ListNode head = new ListNode(-1);
    ListNode cur = head;
    while (list1 != null && list2 != null) {
        if (list1.val <= list2.val) {
            cur.next = list1;
            list1 = list1.next;
        } else {
            cur.next = list2;
            list2 = list2.next;
        }
        cur = cur.next;
    }
    if (list1 != null)
        cur.next = list1;
    if (list2 != null)
        cur.next = list2;
    return head.next;
}

25.树的子结构

题目描述:
输入两棵二叉树A和B,判断B是不是A的子结构。
例如:
图中所示的两棵二叉树,由于A中有一部分子树的结构和B 是一样的,因此B是A的子结构。
剑指offer之JAVA版---史上最全_第9张图片

思路分析:
首先查找A树中与B树中根结点的值一样的结点,如果找到了相同的根结点,递归调用函数判断子结点是否相同,递归的终止条件是达到了A树或者B树的叶结点。同时考虑代码的鲁棒性,对A树或B树为空时的情况进行处理。

public boolean HasSubtree(TreeNode root1, TreeNode root2) {
    if (root1 == null || root2 == null)
        return false;
    return isSubtreeWithRoot(root1, root2) || HasSubtree(root1.left, root2) || HasSubtree(root1.right, root2);
}
//根节点一样,根节点的左右节点也一样
//root2先遍历为空,或者到了叶节点,说明root2是root1的子树;root1先遍历为空,说明没有找到root2结构的子树
private boolean isSubtreeWithRoot(TreeNode root1, TreeNode root2) {
    if (root2 == null)
        return true;
    if (root1 == null)
        return false;
    if (root1.val != root2.val)
        return false;
    return isSubtreeWithRoot(root1.left, root2.left) && isSubtreeWithRoot(root1.right, root2.right);
}

26.二叉树的镜像

题目描述:
请完成一个函数,输入一个二叉树,该函数输出它的镜像。如下图:
在这里插入图片描述

思路分析:
在这里插入图片描述

总结上面的过程,我们得出求一棵树的镜像的过程:我们先前序遍历这棵树的每个结点,如果遍历的结点有子节点,就交换它的两个子节点,当交换完所有的非叶子结点的左右子节点之后,我们就得到了镜像。

public void Mirror(TreeNode root) {
    if (root == null)
        return;
    swap(root);
    Mirror(root.left);
    Mirror(root.right);
}
private void swap(TreeNode root) {
    TreeNode t = root.left;
    root.left = root.right;
    root.right = t;
}

27.对称的二叉树

题目描述:
请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树和它的镜像是一样的,那么它是对称的。
剑指offer之JAVA版---史上最全_第10张图片

思路分析:
对于一棵二叉树,从根结点开始遍历,
① 如果左右子结点有一个为NULL,那么肯定不是对称二叉树;
② 如果左右子结点均不为空,但不相等,那么肯定不是对称二叉树;
③ 如果左右子结点均不为空且相等,那么 ;
遍历左子树,遍历顺序为:当前结点,左子树,右子树;
遍历右子树,遍历顺序为:当前结点,右子树,左子树;
如果遍历左子树的序列和遍历右子树的序列一样,那么该二叉树为对称的二叉树。

boolean isSymmetrical(TreeNode pRoot) {
    if (pRoot == null)
        return true;
    return isSymmetrical(pRoot.left, pRoot.right);
}
boolean isSymmetrical(TreeNode t1, TreeNode t2) {
    if (t1 == null && t2 == null)
        return true;
    if (t1 == null || t2 == null)
        return false;
    if (t1.val != t2.val)
        return false;
    return isSymmetrical(t1.left, t2.right) && isSymmetrical(t1.right, t2.left);
}

28.顺时针打印矩阵

题目描述:
下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10。
思路分析:
对于m行n列的矩阵来说,
第一步:打印(1,1),(1,2),(1,3)…(1,n);
第二步:打印(2,n),(3,n),(4,n)…(m,n);
第三步:打印(m,n-1),(m,n-2),(m,n-3)…(m,1)
第四步:打印(m-1,1),(m-2,1),(m-3,1)…(2,1)
上面四步打印完了一圈,将起始列+1,终止列-1;起始行+1,终止行-1,再循环上面四步操作。
剑指offer之JAVA版---史上最全_第11张图片

public ArrayList printMatrix(int[][] matrix) {
    ArrayList ret = new ArrayList<>();
    int r1 = 0, r2 = matrix.length - 1, c1 = 0, c2 = matrix[0].length - 1;
    while (r1 <= r2 && c1 <= c2) {
        for (int i = c1; i <= c2; i++)
            ret.add(matrix[r1][i]);
        for (int i = r1 + 1; i <= r2; i++)
            ret.add(matrix[i][c2]);
        //判断是否只剩一行,多于一行执行里面的for
        if (r1 != r2)
            for (int i = c2 - 1; i >= c1; i--)
                ret.add(matrix[r2][i]);
        //判断是否只剩一列,多于一列执行里面的for
        if (c1 != c2)
            for (int i = r2 - 1; i > r1; i--)
                ret.add(matrix[i][c1]);
        //起始行、列+1,终止行、列-1
        r1++; r2--; c1++; c2--;
    }
    return ret;
}

29.包含min函数的栈(*)

题目描述:
定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的 min 函数。
思路分析:
第一反应是在栈内添加一个成员变量存放最小值,但是当最小值弹出栈后,我们还需要直到次小值,所以仅仅添加一个成员变量是不可以的,可以新建一个辅助栈将每次的最小值存放起来。(注意:将栈内值排序是不可以的,因为不能保证先进后出,破坏了栈的数据结构。)
剑指offer之JAVA版---史上最全_第12张图片

private Stack dataStack = new Stack<>();
private Stack minStack = new Stack<>();
//压栈
public void push(int node) {
dataStack.push(node);
//minStack.peek()方法返回栈顶元素但不移除
    minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node));
}
//出栈,如果数据栈顶值是当前最小值,那么它和辅助栈顶值相同;数据栈顶值不是最小,辅助栈顶值是重复的上次最小值。所以弹出时可以数据栈和辅助栈同时弹出值
public void pop() {
    dataStack.pop();
    minStack.pop();
}

//取栈顶值
public int top() {
    return dataStack.peek();
}
public int min() {
    return minStack.peek();
}

30.栈的压入、弹出序列

题目描述:
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。
例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不可能是该压栈序列的弹出序列。
思路分析:
判断一个序列是不是栈的弹出顺序的规律:如果下一个弹出的数字刚好是栈顶数字,那么直接弹出。如果下一个弹出的数字不在栈顶,我们把压栈序列中还没有入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止。如果所有的数字都压入栈了仍没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列。
也就是压栈序列全部入栈后,栈中元素没有全部弹出,即栈非空,表示这个弹出序列不是该压栈序列的弹出序列。

public boolean IsPopOrder(int[] pushSequence, int[] popSequence) {
    int n = pushSequence.length;
Stack stack = new Stack<>();

    for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) {
        stack.push(pushSequence[pushIndex]);
        while (popIndex < n && !stack.isEmpty() 
                && stack.peek() == popSequence[popIndex]) {
            stack.pop();
            popIndex++;
        }
    }
    return stack.isEmpty();
}

31.1 从上往下打印二叉树

题目描述:
从上往下打印出二叉树的每个节点,同层节点从左至右打印。
例如,以下二叉树层次遍历的结果为:1,2,3,4,5,6,7。
剑指offer之JAVA版---史上最全_第13张图片

思路分析:
从上到下打印二叉树的规律:每一次打印一个节点的时候,如果该节点有子节点,把该节点的子节点放到一个队列的尾。接下来到队列的头部取出最早进入队列的节点,重复前面打印操作,直到队列中所有的节点都被打印出为止。

public ArrayList PrintFromTopToBottom(TreeNode root) {
    Queue queue = new LinkedList<>();
ArrayList ret = new ArrayList<>();
//将根节点放入队列
    queue.add(root);
    while (!queue.isEmpty()) {
        int cnt = queue.size();
        while (cnt-- > 0) {
            //poll方法取出队列的第一个值并将其从队列中删除
            TreeNode t = queue.poll();
            //有可能某个节点的左子树或右子树为空,为空的话跳出本次循环判断队列的下一个值
            if (t == null)
                //跳出单次循环
                continue;
            ret.add(t.val);
            queue.add(t.left);
            queue.add(t.right);
        }
    }
    return ret;
}

31.2 把二叉树打印成多行

题目描述:
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印 ,每一层打印一行。
思路分析:
这道题和前面的题类似,也可以使用一个队列来保存将要打印的节点。内循环每次添加的结点刚好是一层,将每一层放进一个ArrayList中,再将ArrayList放进队列。

ArrayList> Print(TreeNode pRoot) {
    ArrayList> ret = new ArrayList<>();
    Queue queue = new LinkedList<>();
    queue.add(pRoot);
    while (!queue.isEmpty()) {
        ArrayList list = new ArrayList<>();
        //cnt是每次外循环更新的,1,2,4...
        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);
        }
        //一层的数据放进一个list,再将list放进队列
        if (list.size() != 0)
            ret.add(list);
    }
    return ret;
}

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

题目描述:
请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
思路分析:
在分行打印的基础上,添加一个标识符,打印一行反转一次。

public ArrayList> Print(TreeNode pRoot) {
    ArrayList> ret = new ArrayList<>();
    Queue queue = new LinkedList<>();
queue.add(pRoot);
//设置标识符
    boolean reverse = false;
    while (!queue.isEmpty()) {
        ArrayList 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);
        //一个list一行,隔一个list反转一次
        reverse = !reverse;
        if (list.size() != 0)
            ret.add(list);
    }
    return ret;
}

32.二叉搜索树的后序遍历序列

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

例如,下图是后序遍历序列 1,3,2 所对应的二叉搜索树。
剑指offer之JAVA版---史上最全_第14张图片

思路分析:
已知条件:后序序列最后一个值为root;二叉搜索树左子树值都比root小,右子树值都比root大。
① 确定root;
② 遍历序列(除去root结点),找到第一个大于root的位置,则该位置左边为左子 树,右边为右子树;
③ 遍历右子树,若发现有小于root的值,则直接返回false;
④ 分别判断左子树和右子树是否仍是二叉搜索树(即递归步骤1、2、3)。

public boolean VerifySquenceOfBST(int[] sequence) {
    if (sequence == null || sequence.length == 0)
        return false;
    return verify(sequence, 0, sequence.length - 1);
}
private boolean verify(int[] sequence, int first, int last) {
    if (last - first <= 1)
        return true;
    int rootVal = sequence[last];
    int cutIndex = first;
    while (cutIndex < last && sequence[cutIndex] <= rootVal)
        cutIndex++;
    for (int i = cutIndex; i < last; i++)
        if (sequence[i] < rootVal)
            return false;
    return verify(sequence, first, cutIndex - 1) && verify(sequence, cutIndex, last - 1);
}

33.二叉树中和为某一值的路径(*)

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

下图的二叉树有两条和为 22 的路径:10, 5, 7 和 10, 12
剑指offer之JAVA版---史上最全_第15张图片

思路分析:
也就是说每条满足条件的路径都是以根节点开始,叶子结点结束,如果想得到所有根节点到叶子结点的路径(不一一定满足和为某整数的条件),需要遍历整棵树,还要先遍历根节点,所以采用先序遍历。

以上面的树模拟先序遍历的过程:
10–>5–>4 已近到达叶子结点,不满足要求22,因此该路径访问结束,需要访问下一个路径;
因为在访问节点的过程中,我们并不知道该路径是否满足要求,所以我们每访问一个节点就要记录该结点
访问下有一个结点前,要先从结点4退回到结点5,再访问下一个结点7,因为4不在去往7的路径上,所以要在路径中将4删除;
10–>5–>7 满足要求,保存该路径;
访问下一个结点,从结点7回到结点5再回到结点10;
10–>12,满足要求,保存该路径。

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

public ArrayList> FindPath(TreeNode root, int target) {
    backtracking(root, target, new ArrayList<>());
    return ret;
}
private void backtracking(TreeNode node, int target, ArrayList path) {  
if (node == null )
        return;

   //每访问到一个结点的时候,都把当前的结点添加到路径中去,并调整target的值
    path.add(node.val);
target -= node.val;
//已到叶节点并且和为target,则把当前路径添加到输出列表里
//因为add添加的是引用,如果不new一个的话,最终list保存到的只是最后一个path
if (target == 0 && node.left == null && node.right == null) {
    //注意这里要new,否则ret中只是引用,后面值的改变会覆盖前面的值
        ret.add(new ArrayList<>(path));
} else {
    //否则继续遍历
        backtracking(node.left, target, path);
        backtracking(node.right, target, path);
}
      //什么时候执行????
//已到叶节点之后会跳过两个递归函数到这里,此时要把最后一个结点从路径中删除,才能返回上层结点    path.remove(path.size() - 1);
}

34.复杂链表的复制

题目描述:
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。

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

    RandomListNode(int label) {
        this.label = label;
    }
}

在这里插入图片描述

思路分析:
第一步,在每个节点的后面插入复制的节点。
在这里插入图片描述

第二步,对复制节点的 random 链接进行赋值。
在这里插入图片描述

第三步,拆分。
在这里插入图片描述

public RandomListNode Clone(RandomListNode pHead) {
    if (pHead == null)
        return null;
    // 插入新节点
    RandomListNode cur = pHead;
    while (cur != null) {
        RandomListNode clone = new RandomListNode(cur.label);
        //将复制结点插入链表中
        clone.next = cur.next;
        cur.next = clone;
        //当前结点指向复制结点的下一个结点,进行下一轮复制插入 
        cur = clone.next;
    }
// 建立 random 链接                                                                                               
//从头结点开始
    cur = pHead;
while (cur != null) {
    // 拿到复制结点
        RandomListNode clone = cur.next;
        if (cur.random != null)
            //将当前复制结点的random指向复制的random,所以是cur.random.next
            clone.random = cur.random.next;
        //当前结点指向复制结点的下一个结点,
        cur = clone.next;
    }
// 拆分
//从头结点开始
    cur = pHead;
    RandomListNode pCloneHead = pHead.next;
    while (cur.next != null) {
        RandomListNode next = cur.next;
        //当前结点指向下下个结点
        cur.next = next.next;
        //当前结点移向下一个结点
//1->1’->2->2’...第一次循环1->2->2’,1’->2->2’,第二次循环1->2->2’,1’->2’
        cur = next;
    }
    return pCloneHead;
}

35.二叉搜索树与双向链表(*)

题目描述:
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。
在这里插入图片描述

思路分析:
在二叉树中,每个结点都有两个指向子节点的指针。在双向链表中,每个结点也有两个指针,他们分别指向前一个结点和后一个结点。
在搜索二叉树中,左子结点的值总是小于父节点的值,右子节点的值总是大于父节点的值。因此我们在转换成排序的双向链表时,原先指向的左子结点的指针调整为链表中指向前一个结点的指针,原先指向右子节点的指针调整为指向后一个结点的指针。

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);
//当前结点的left指向当前结点的前一个结点
    node.left = pre;
if (pre != null)
    //前一个结点的right指向当前结点
        pre.right = node;
    // 后移一个结点
    pre = node;
    if (head == null)
        head = node;
    inOrder(node.right);
}

36.序列化二叉树

题目描述:
请实现两个函数,分别用来序列化和反序列化二叉树。
剑指offer之JAVA版---史上最全_第16张图片

思路分析:
题目实际上就是用序列来表示一棵二叉树,然后还可以根据这个序列重建二叉树。对于上图中的树,以前序遍历为例,先访问到1,然后2,然后4,4的左右子结点都为空,可以用一个特殊字符 替 代 , 所 以 上 图 中 的 二 叉 树 前 序 遍 历 表 示 就 是 “ 1 , 2 , 4 , 替代,所以上图中的二叉树前序遍历表示就是“1,2,4, 1,2,4,, , , ,,3,5, , , ,,6, , , ,”。
重建的时候,访问的第一个结点为根结点,接下来的数字是2,它是根结点的左子结点。接下来的是4,它是2的左子结点。然后遇到两个 , 说 明 4 的 左 右 子 结 点 都 是 N U L L 。 接 下 来 结 点 回 退 , 访 问 4 的 父 结 点 2 , 又 是 ,说明4的左右子结点都是NULL。接下来结点回退,访问4的父结点2,又是 4NULL退访42,说明2的右子结点是NULL。再返回到根结点,这时候该建立它的右子结点了,下一个数值是3,说明3是根结点的右子结点,剩下的步骤和左子树部分类似。

private String deserializeStr;

/*  序列化函数  */
public String Serialize(TreeNode root) {
    //结点为空时,用$代替
    if (root == null)
        return "$";
    //每个结点之间用空格隔开
    return root.val + " " + Serialize(root.left) + " " + Serialize(root.right);
}

/*  反序列化函数  */
public TreeNode Deserialize(String str) {
    deserializeStr = str;
    return Deserialize();
}
private TreeNode Deserialize() {
    if (deserializeStr.length() == 0)
        return null;
    //index表示第一个空格所在的索引
int index = deserializeStr.indexOf(" ");
//利用index切割下结点
    String node = index == -1 ? deserializeStr : deserializeStr.substring(0, index);
    //更新序列化的deserializeStr 
deserializeStr = index == -1 ? "" : deserializeStr.substring(index + 1);

    if (node.equals("$"))
        return null;
    //获取结点的val
int val = Integer.valueOf(node);
//构建结点
TreeNode t = new TreeNode(val);
//递归构建结点的左右子树
    t.left = Deserialize();
    t.right = Deserialize();
    return t;
}

37.字符串的排列

题目描述:
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a, b, c 所能排列出来的所有字符串 abc, acb, bac, bca, cab 和 cba。
思路分析:
我们求整个字符串的排列,可以看成两步:首先求出所有可能出现在第一个位置的字符,即把第一个字符和后面所有的字符交换。第二步固定第一个字符,求后面所有字符的排列。这个时候我们仍把后面的所有字符分成两部分:后面字符的第一个字符,以及这个字符之后的所有字符。然后把第一个字符逐一和它后面的字符交换。
解法一:

 public class Solution {
     
    public ArrayList Permutation(String str) {
        ArrayList result = new ArrayList() ;
        if(str==null || str.length()==0) { return result ; }
 
        char[] chars = str.toCharArray() ;
        TreeSet temp = new TreeSet<>() ;
        Permutation(chars, 0, temp);
        result.addAll(temp) ;
        return result ;
    }
 
    public void Permutation(char[] chars, int begin, TreeSet result) {
        if(chars==null || chars.length==0 || begin<0 || begin>chars.length-1) { return ; }
         //set自动判断“当前排列”和“集合中的排列”是否重复
        if(begin == chars.length-1) {
            result.add(String.valueOf(chars)) ;
        }else {
            for(int i=begin ; i<=chars.length-1 ; i++) {
                 //求出所有可能出现在第一个位置的字符
                swap(chars, begin, i) ;
                 //对第二个位置的字符同理递归调用
                Permutation(chars, begin+1, result);
                  //复位,在最后交换完成一个排列后,回到初始状态进行下一次循环
                swap(chars, begin, i) ;
            }
        }
    }
 
    public void swap(char[] x, int a, int b) {
        char t = x[a];
        x[a] = x[b];
        x[b] = t;
    }
     
}

解法二:
设置一个标识符hasUsed,对每个位置依次插入不同的字符,插入后hasUsed设置为true,防止排列重复使用一个字符。

private ArrayList ret = new ArrayList<>();
public ArrayList 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;
    }
}

38.数组中出现次数超过一半的数字

题目描述:
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
例子说明:如输入一个长度为9的数组{ 1, 2, 3, 2, 2, 2, 5, 4, 2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2 。
思路分析:
**方法一:**数组中有一个数字出现的次数超过了数组长度的一半。如果把这个数组排序,那么出现次数超过数组长度的一半的数字一定位于数组的中间。拿到这个数字,遍历数组中的数,统计这个数字出现的次数是否大于n/2。

import java.util.*;
public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        int len=array.length;
        if(len<1){
            return 0;
        } 
        int count=0;
         //对int数组排序,按照从小到大的顺序
        Arrays.sort(array);
        int num=array[len/2];
        for(int i=0;i

方法二:多数投票算法。
① majority赋值为数组第一个元素,cnt赋初值为1
② 从数组第二个元素开始遍历,如果majority和当前数组元素值相同,则cnt++, 反之cnt–;
③ 如果cnt==0,则将majority的值设置为数组的当前元素;
④ 重复上述两步,直到扫描完数组。
⑤ count赋值为0,再次从头扫描数组,如果素组元素值与majority的值相同则count++,直到扫描完数组为止。
⑥ 如果此时count的值大于等于n/2,则返回majority的值,反之则返回0。

算法原理:
举个例子,我们的输入数组为[1,1,0,0,0,1,0],那么0就是多数元素。
首先,majority被设置为第一个元素1,cnt也变成1,由于1不是多数元素,所以当扫描到数组某个位置时,cnt一定会减为0。
当cnt变成0时,对于每一个出现的1,我们都用一个0与其进行抵消,所以我们消耗掉了与其一样多的0。由于求得是出现次数大于数组长度的一半,所以满足条件的majority对应的cnt一定是大于0的。

public int MoreThanHalfNum_Solution(int[] nums) {
    int majority = nums[0];
    for (int i = 1, cnt = 1; i < nums.length; i++) {
        cnt = nums[i] == majority ? cnt + 1 : cnt - 1;
        if (cnt == 0) {
            majority = nums[i];
            cnt = 1;
        }
    }
    int cnt = 0;
    for (int val : nums)
        if (val == majority)
            cnt++;
    return cnt > nums.length / 2 ? majority : 0;
}

39.最小的K个数(含快排)

题目描述:
输入n个整数,找出其中最小的k个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。
思路分析:
方法一:快速选择(只有当允许修改数组元素时才可以使用)
快速排序的 partition() 方法,会返回一个整数 j 使得 a[l…j-1] 小于等于 a[j],且 a[j+1…h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素,这种找第 K 个元素的算法称为快速选择算法。

public ArrayList GetLeastNumbers_Solution(int[] nums, int k) {
    ArrayList ret = new ArrayList<>();
    if (k > nums.length || k <= 0)
        return ret;
    findKthSmallest(nums, k - 1);
    /* findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数 */
    for (int i = 0; i < k; i++)
        ret.add(nums[i]);
    return ret;
}
public void findKthSmallest(int[] nums, int k) {
    int low = 0, high = nums.length - 1;
    while (low < high ) {
        int j = partition(nums, low , high );
        if (j == k)
            break;
        if (j > k)
            high = j - 1;
        else
            low = j + 1;
    }
}
private int partition(int[] nums, int low, int high) {
    //确定一个基准数p
    int p = nums[low];     /* 切分元素 */
    int i = low, j = high + 1;
    while (true) {
        while (i != high && nums[++i] < p) ;
        while (j != low && nums[--j] > p) ;
        if (i >= j)
            break;
        //当i>=p,j<=p时交换i、j指向的值
        swap(nums, i, j);
}
//两指针相遇时,交换基准数和两指针指向的值
    swap(nums, low, j);
    return j;
}
private void swap(int[] nums, int i, int j) {
    int t = nums[i];
    nums[i] = nums[j];
    nums[j] = t;
}

方法二:大小为K的最小堆
当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆。 当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆。
可以创建一个容器来存储最小的k个数,如果容器中数字少于k个,那么直接将读到的数值放入容器;如果已经有了k个数了,找出k个数中的最大值,将待插入的值和最大值比较,如果小于最大值,那么替换当前最大值,如果大于最大值,那么将待插入值抛弃。

public ArrayList GetLeastNumbers_Solution(int[] nums, int k) {
    if (k > nums.length || k <= 0)
        return new ArrayList<>();

    //PriorityQueue默认是一个小顶堆,然而可以通过传入自定义的Comparator函数来实现大顶堆
    PriorityQueue maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
    for (int num : nums) {
        maxHeap.add(num);
        if (maxHeap.size() > k)
           //maxHeap最头上的值是最大值,当个数超过k个时,将最大的值弹出去(删除)
            maxHeap.poll();
    }
    return new ArrayList<>(maxHeap);
}

40.1 数据流中的中位数()

题目描述:
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
思路分析:
剑指offer之JAVA版---史上最全_第17张图片

可以看到P1指向的是左半部分的最大值,P2指向的是右半部分的最小值,因此左半部分可以用一个最大堆,右半部分可以用一个最小堆。
并且保证数据平均分配到两个堆,两个堆中的数据个数差不能超过1,且最大堆的数值都小于最小堆的数值。

/* 大顶堆,存储左半边元素 */
private PriorityQueue left = new PriorityQueue<>((o1, o2) -> o2 - o1);
/* 小顶堆,存储右半边元素,并且右半边元素都大于左半边 */
private PriorityQueue 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();
}

40.2 字符流中第一个不重复的字符(*)

题目描述:
请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符 “go” 时,第一个只出现一次的字符是 “g”。当从该字符流中读出前六个字符“google" 时,第一个只出现一次的字符是 “l”。
思路分析:
利用一个数组,然后数组cnts的下标就是这个字符对应的整数。这么进来一个字符,就将判断该字符对应下标在数组中的值是否已经有,有就在基础上加一,如果没有,则直接置为1。

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

public void Insert(char ch) {
   
cnts[ch]++;
    //将字符放入队列中缓存,记录字符的顺序
queue.add(ch);
//按照字符顺序,字符出现大于1次的从队列中删除
    while (!queue.isEmpty() && cnts[queue.peek()] > 1)
        queue.poll();
}
public char FirstAppearingOnce() {
     //看最终队列是否为空,不为空的话,最头上的就是最先出现的不重复字符
    return queue.isEmpty() ? '#' : queue.peek();
}

41 .连续子数组的最大和

题目描述:
输入一个整型数组,数组里有正数也有负数。数组中一个或连续的多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。例如输入的数组为{1,-2,3,10,-4,7,2,-5},和最大的子数组为{3,10,-4,7,2},因此输出为该子数组的和18。
思路分析:
从第一个数开始累加,如果当前累加和sum小于0,就抛弃之前的累加,从下一个数开始累加,否则,就将本次累加和上次累加的较大值存到greatestSum中。

public int FindGreatestSumOfSubArray(int[] nums) {
    if (nums == null || nums.length == 0)
        return 0;
    //初值赋Integer的最小值
    int greatestSum = Integer.MIN_VALUE;
    int sum = 0;
    for (int val : nums) {
        sum = sum <= 0 ? val : sum + val;
        //greatestSum 记录上次循环的最大和
        greatestSum = Math.max(greatestSum, sum);
    }
    return greatestSum;
}

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

题目描述:
输入一个整数n,求从1到n个整数的十进制表示中1出现的次数。例如输入12,从1到12这些整数中包含1的数字有1,10,11,和12。一共出现了5次。

思路分析:
对于一个数字来说,分别从个位、百位、千位…考虑为1的情况。我们从一个5位的数字讲起,先考虑其百位为1的情况。分3种情况讨论:
① 百位数字>=2 example: 31256 当其百位为>=时,有以下这些情况满足 (为方便起见,计312为a,56为b):
100 ~ 199
1100 ~ 1199

31100 ~ 31199
余下的都不满足!
因此,百位>=2的5位数字,其百位为1的情况有(a/10+1)*100个数。字 (a/10+1)=>对应于 0 ~ 31,且每一个数字,对应范围是100个数(末尾0-99)
② 百位数字 ==1 example: 31156 当其百位为1时,有以下这些情况满足:
100 ~ 199
1100 ~ 1199

30100 ~ 30199
31100 ~ 31156
因此,百位为1的5位数字,共有(a/10)*100+(b+1)。
③ 百位数字 ==0 example: 31056 当其百位为0时,有以下这些情况满足:
100 ~ 199
1100 ~ 1199
30100 ~ 30199
其余都不满足,因此,百位数为0的5位数字,共有(a/10)*100个数字。
我们可以进一步统一以下表达方式,即当百位>=2或=0时,有[(a+8)/10]*100;当百位=1时,有[(a+8)/10]*100+(b+1)。用代码表示就是: [(a+8)/10]*100+(a%10==1)?(b+1):0;

为什么要加8呢?因为只有大于2的时候才会产生进位等价于(a/10+1),当等于0和1时就等价于(a/10)。另外,等于1时要单独加上(b+1),这里我们用a对10取余是否等于1的方式判断该百位是否为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;
}

43.数字序列中的某一位数字

题目描述:
数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从0开始计数)是5,第13位是1,第19位是4,等等。请写一个函数,求任意第n位对应的数字。
思路分析:
例如要找序列的第1001位是什么,那么可以如下分析:
① 0~9共10位数,目标数字显然不在这个范围,往后找1001-10=991位;
② 10~99共90×2=180位数,目标数字也不在这个范围,继续往后找991-180=811位;
③ 100~199共90×3=2700位数,可知目标数在这个范围内,有811=270×3+1,意味 着第811位是从100开始的第270个数字的中间一位,也就是370的中间一位,7。

public int getDigitAtIndex(int index) {
    if (index < 0)
        return -1;
    int place = 1;  // 1 表示个位,2 表示 十位...
    while (true) {
        int amount = getAmountOfPlace(place);
        //得到place位的数共占多少位
        int totalAmount = amount * place;
        if (index < totalAmount)
            return getDigitAtIndex(index, place);
        index -= totalAmount;
        place++;
    }
}
/** 
* place 位数的数字组成的字符串长度 * 10, 90, 900, ... 
*/
private int getAmountOfPlace(int place) {
    //个位数的数字长度是0-9共10个
    if (place == 1)
        return 10;
//Math.pow(a,b)求得是a的b次方
    return (int) Math.pow(10, place - 1) * 9;
}
/** 
* place 位数的起始数字 * 0, 10, 100, ... 
*/
private int getBeginNumberOfPlace(int place) {
    if (place == 1)
        return 0;
    return (int) Math.pow(10, place - 1);
}
/** 
* 在 place 位数组成的字符串中,第 index 个数 
*/
private int getDigitAtIndex(int index, int place) {
    //找到place位数范围的起始点
int beginNumber = getBeginNumberOfPlace(place);
//找到目标数字是位于从起始点开始第几个数
int shiftNumber = index / place;
//算出这个数的值
String number = (beginNumber + shiftNumber) + "";
//位于该数的第几位
    int count = index % place;
    return number.charAt(count) - '0';
}

44.把数组排成最小的数

题目描述:
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组 {3,32,321},则打印出这三个数字能排成的最小数字为 321323。
思路分析:
可以看成是一个排序问题,在比较两个字符串 S1 和 S2 的大小时,应该比较的是 S1+S2 和 S2+S1 的大小,如果 S1+S2 < S2+S1,那么应该把 S1 排在前面,否则应该把 S2 排在前面。

public String PrintMinNumber(int[] numbers) {
    if (numbers == null || numbers.length == 0)
        return "";
int n = numbers.length;
//新建一个同长度的string类型的数组,防止拼接后的数值溢出
    String[] nums = new String[n];
for (int i = 0; i < n; i++)
    //将int型数组中的数值逐个复制到string数组中
        nums[i] = numbers[i] + "";
     //对nums中的数从小到大排序
    Arrays.sort(nums, (s1, s2) -> (s1 + s2).compareTo(s2 + s1));
    String ret = "";
    for (String str : nums)
        ret += str;
    return ret;
}

45.把数字翻译成字符串

题目描述:
给定一个数字,按照如下规则翻译成字符串:1翻译成“a”,2翻译成“b”… 26 翻译成“z”。一个数字有多种翻译可能,例如 23369 一共有 5 种,分别是 bccfi,bwfi,bczi,mcfi,mzi。实现一个函数,用来计算一个数字有多少种不同的翻译方法。
思路分析:
如果定义函数f(i)表示从第i位数组开始的不同翻译的数目,那么:
f(i) = f(i+1) +g(i,i+1)×f(i+2)
利用递归可以实现上面的算法,但是会有很多重复计算,所以考虑动态规划:
f(i) = f(i-1) +g(i-1,i)×f(i-2) (i>2时)
其中当第i位和第i+1位两位数字拼接起来的数字小于等于26时,函数g(i,i+1)=1,否则g(i,i+1)=0。

public int numDecodings(String s) {
    if (s == null || s.length() == 0)
        return 0;
int n = s.length();
//新建一个数组dp用来存放0~n个数字翻译成字符串的方法数
    int[] dp = new int[n + 1];
    dp[0] = 1;
dp[1] = s.charAt(0) == '0' ? 0 : 1;
//i>=2时开始使用动态规划
for (int i = 2; i <= n; i++) {
    //substring(x,y)表示从x开始到y前结束的子字符串
        int one = Integer.valueOf(s.substring(i - 1, i));
        if (one != 0)
            //第一种情况是前一位i-1单个数字翻译成字符
            dp[i] += dp[i - 1];
        //如果前面的数字为0就跳出本次循环
        if (s.charAt(i - 2) == '0')
            continue;
        int two = Integer.valueOf(s.substring(i - 2, i));
        //如果前两位i-1和i-2两位数拼起来小于等于26就两个数字翻译成一个字符
        if (two <= 26)
            dp[i] += dp[i - 2];
    }
    return dp[n];
}

46.礼物的最大价值

题目描述:
在一个 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
思路分析:
应该用动态规划求解,而不是深度优先搜索,深度优先搜索过于复杂,不是最优解。
定义函数f(i,j) 表示到达坐标(i,j)的格子时能拿到的礼物总和的最大值,那么根据题目,有两条道路可以到达(i,j):(i-1,j)或(i,j-1)。所以得到公式:
f(i,j) = max ( f( i-1 ,j ), f( i ,j-1 ))+gift [i ,j ]
其中gift [i ,j ]表示坐标(i,j)的格子里的礼物价值。

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];
}

47.最长不含重复字符的子字符串(*)

题目描述:
输入一个字符串(只包含 a~z 的字符),求其最长不含重复字符的子字符串的长度。例如对于 arabcacfr,最长不含重复字符的子字符串为 acfr,长度为 4。
思路分析:
使用位图及快慢指针来查找子串,位图存储快慢指针之间的字符。
使用快慢指针:
① 慢指针不动,快指针先走,每走一步,判断快指针所指字符在位图中是否已经存在,不存在,存储字符信息并继续走,直到快指针指向重复字符串;
② 若此时快指针 - 慢指针 > 已经记录的字符串长度,更新字符串长度;
③ 快指针不动,慢指针开始走,每走一步,将位图中对应的字符信息删除,直至快慢指针所指的字符相同(此时快慢指针依旧是错开的,慢指针指向快指针这个字符出现的第一次,快指针指向的是字符出现第二次),此时不删除该字符的信息,慢指针直接加加;
④ 重复步骤1、2、3,直至快指针走到字符串尾,当快指针指向尾,再判断一次步骤2,返回子串串长度。

public int longestSubStringWithoutDuplication(String str) {
    int curLen = 0;
int maxLen = 0;
//数组记录每个字符在字符串中最后出现的位置
int[] preIndexs = new int[26];
// Arrays.fill(preIndexs, -1)将preIndexs的每个初值填充为-1
Arrays.fill(preIndexs, -1);
//curI 是一个快指针,表示字符的当前位置,preI 是一个慢指针,表示当前字符上一次出现的位置
    for (int curI = 0; curI < str.length(); curI++) {
        int c = str.charAt(curI) - 'a';
        int preI = preIndexs[c];
        if (preI == -1 || curI - preI > curLen) {
            curLen++;
        } else {
            maxLen = Math.max(maxLen, curLen);
            curLen = curI - preI;
        }
        preIndexs[c] = curI;
    }
    maxLen = Math.max(maxLen, curLen);
    return maxLen;
}

48.丑数

题目描述:
把只包含因子 2、3 和 5 的数称作丑数(Ugly Number)。例如 6(2×3)、8 (2×2×2)都是丑数,但 14(2×7) 不是,因为它包含因子 7。习惯上我们把 1 当做是第一个丑数。求按从小到大的顺序的第 N 个丑数。
思路分析:
可以创建数组保存已经找到的丑数,用空间换时间。可以创建一个数组,里面的数字是排好序的丑数,每个丑数都是前面的丑数乘以2、3或5得到的。
重点是怎么保证数组是有序的,考虑把已有的丑数乘2记为M2,乘3记为M3,乘5记为M5,取三个数中的最小值。

public int GetUglyNumber_Solution(int N) {
    if (N <= 6)
        return N;
    int i2 = 0, i3 = 0, i5 = 0;
    int[] dp = new int[N];
    dp[0] = 1;
    for (int i = 1; i < N; i++) {
        int next2 = dp[i2] * 2, next3 = dp[i3] * 3, next5 = dp[i5] * 5;
        dp[i] = Math.min(next2, Math.min(next3, next5));
        if (dp[i] == next2)
            i2++;
        if (dp[i] == next3)
            i3++;
        if (dp[i] == next5)
            i5++;
    }
    return dp[N - 1];
}

49.第一个只出现一次的字符位置

题目描述:
在一个字符串中找到第一个只出现一次的字符,并返回它的位置。
思路分析:
可以定义哈希表的键是字符,值是字符出现的次数。还需要从头开始扫描字符串两次,第一次扫描时,每扫描到一个字符就在哈希表中把对应字符的次数加1,第二次扫描的时,第一个只出现一次的字符就是符合要求的输出。
因为字符数量有限,可以用int数组代替哈希表。

public int FirstNotRepeatingChar(String str) {
int[] cnts = new int[256];
//第一次遍历,统计字符出现的次数
    for (int i = 0; i < str.length(); i++)
        cnts[str.charAt(i)]++;
    //第二次遍历,得到最先出现的数量为1的字符
    for (int i = 0; i < str.length(); i++)
        if (cnts[str.charAt(i)] == 1)
            return i;
    return -1;
}

以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。

public int FirstNotRepeatingChar2(String str) {
    //新建两位bit位00,足够表示0,1,大于等于2了
    BitSet bs1 = new BitSet(256);
    BitSet bs2 = new BitSet(256);
    for (char c : str.toCharArray()) {
        if (!bs1.get(c) && !bs2.get(c))
           //将c对应数字的索引处设置为true
            bs1.set(c);     // 0 0 -> 0 1
        else if (bs1.get(c) && !bs2.get(c))
            bs2.set(c);     // 0 1 -> 1 1
    }
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (bs1.get(c) && !bs2.get(c))  // 0 1
            return i;
    }
    return -1;
}

50.数组中的逆序对

题目描述:
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007
思路分析:
先把数组分割成子数组,先统计出子数组内部的逆序对的数目,然后再统计出两个相邻子数组之间的逆序对的数目。在统计逆序对的过程中,还需要对数组进行排序。如果对排序算法很熟悉,我们不难发现这个过程实际上就是归并排序。

private long cnt = 0;
private int[] tmp;  // 在这里声明辅助数组,而不是在 merge() 递归函数中声明

public int InversePairs(int[] nums) {
    tmp = new int[nums.length];
    mergeSort(nums, 0, nums.length - 1);
    return (int) (cnt % 1000000007);
}
/*  
将数组分成前后两部分,将子数组再分前后两部分,直到1个元素  
*/
private void mergeSort(int[] nums, int l, int h) {
    if (h - l < 1)
        return;
    int m = l + (h - l) / 2;
    mergeSort(nums, l, m);
    mergeSort(nums, m + 1, h);
    merge(nums, l, m, h);
}
/* 
将子数组逆序对求出,合并子数组,对子数组排序,同时统计逆序对  
*/
private void merge(int[] nums, int l, int m, int h) {
    int i = l, j = m + 1, k = l;
    while (i <= m || j <= h) {
        if (i > m)
            tmp[k] = nums[j++];
        else if (j > h)
            tmp[k] = nums[i++];
        else if (nums[i] < nums[j])
            tmp[k] = nums[i++];
        else {
            tmp[k] = nums[j++];
            this.cnt += m - i + 1;  // nums[i] >= nums[j],说明 nums[i...mid] 都大于 nums[j]
        }
        k++;
    }
    for (k = l; k <= h; k++)
        nums[k] = tmp[k];
}

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

题目描述:
输入两个链表找出它们的第一个公共结点。
在这里插入图片描述

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

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

public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
    ListNode l1 = pHead1, l2 = pHead2;
    while (l1 != l2) {
        l1 = (l1 == null) ? pHead2 : l1.next;
        l2 = (l2 == null) ? pHead1 : l2.next;
    }
    return l1;
}

52.数字在排序数组中出现的次数

题目描述:
统计一个数字在排序数组中出现的次数。
Input:
nums = 1, 2, 3, 3, 3, 3, 4, 6
K = 3

Output:
4
思路分析:
既然是有序数组,首先考虑二分法查找。因为data中都是整数,所以可以稍微变一下,不是搜索k的两个位置,而是搜索k和k+1这两个数应该插入的位置,然后相减即可。

public int GetNumberOfK(int[] nums, int K) {
    int first = binarySearch(nums, K);
    int last = binarySearch(nums, K + 1);
    return (first == nums.length || nums[first] != K) ? 0 : last - first;
}
private int binarySearch(int[] nums, int K) {
    int l = 0, h = nums.length;
    while (l < h) {
        int m = l + (h - l) / 2;
        if (nums[m] >= K)
            h = m;
        else
            l = m + 1;
    }
    return l;
}

53.二叉查找树的第K大的结点

题目描述:
给定一棵二叉查找树,找出其中第K大的结点。
思路分析:
根据二叉查找树的左子节点小于根节点,右子节点大于根节点的特点,中序遍历二叉查找树后,数组是有序的。

private TreeNode ret;
private int cnt = 0;

public TreeNode KthNode(TreeNode pRoot, int k) {
    inOrder(pRoot, k);
    return ret;
}

private void inOrder(TreeNode root, int k) {
    if (root == null || cnt >= k)
        return;
    inOrder(root.left, k);
    cnt++;
    if (cnt == k)
        ret = root;
    inOrder(root.right, k);
}

54.1 二叉树的深度

题目描述:
从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

思路分析:
剑指offer之JAVA版---史上最全_第18张图片

public int TreeDepth(TreeNode root) {
    return root == null ? 0 : 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right));
}

54.2 平衡二叉树

题目描述:
输入一棵二叉树的根结点,判断该树是不是平衡二叉树。
如果某二叉树的任意节点的左、右子树的深度相差不超过1,那么它就是一棵平衡二叉 树。
思路分析:
采用后序遍历方式遍历二叉树的每个结点,那么遍历到一个结点之前我们就已经遍历了它的左、右子树,只要在遍历每个结点时记录它的深度,就可以一边遍历一遍判断每个结点是否平衡。

private boolean isBalanced = true;
public boolean IsBalanced_Solution(TreeNode root) {
    height(root);
    return isBalanced;
}

private int height(TreeNode root) {
    if (root == null || !isBalanced)
        return 0;
    int left = height(root.left);
    int right = height(root.right);
    if (Math.abs(left - right) > 1)
        isBalanced = false;
    return 1 + Math.max(left, right);
}

55 . 数组中只出现一次的数字(*)

题目描述:
一个整型数组里除了两个数字之外,其他的数字都出现了两次,找出这两个只出现一次的数字。
思路分析:
根据异或运算的性质:任何一个数字异或它本身都等于0。如果我们从头到尾依次异或数组中的每个数字,那么最终结果刚好是只出现一次的两个数字的异或值。异或值为1的位置,做异或运算两个数在相应位置上一个是1一个是0。
所以根据异或的结果1所在的位置把数组分半,对应位置为1的一组,为0的一组,成对出现的数字一定在一组,只出现一次的数会分别在两组,这样继续对每一半相异或则可以分别求出两个只出现一次的数字。
diff &= -diff 得到出 diff 不为 0 的位,也就是不存在重复的两个元素在位级表示上那一位,利用这一位就可以将两个元素区分开来。

public void FindNumsAppearOnce(int[] nums, int num1[], int num2[]) {
int diff = 0;
//diff与数组中元素依次异或,最终得到的结果是两个只出现一次数值的异或值
//
    for (int num : nums)
        diff ^= num;
   //????
    diff &= -diff;
    for (int num : nums) {
        if ((num & diff) == 0)
            num1[0] ^= num;
        else
            num2[0] ^= num;
    }
}

56.1 和为S的两个数字

题目描述:
输入一个递增排序的数组和一个数字 S,在数组中查找两个数,使得他们的和正好是 S。如果有多对数字的和等于 S,输出两个数的乘积最小的。
思路分析:
使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。

  • 如果两个指针指向元素的和 sum == target,那么得到要求的结果;
  • 如果 sum > target,移动较大的元素,使 sum 变小一些;
  • 如果 sum < target,移动较小的元素,使 sum 变大一些。
public ArrayList FindNumbersWithSum(int[] array, int sum) {
    int i = 0, j = array.length - 1;
    while (i < j) {
        int cur = array[i] + array[j];
        if (cur == sum)
            //Arrays.asList(“a”,“b”)是把数组转换成集合
            return new ArrayList<>(Arrays.asList(array[i], array[j]));
        if (cur < sum)
            i++;
        else
            j--;
    }
    return new ArrayList<>();
}

56.2 和为S的连续正数序列

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

例如和为 100 的连续序列有:
[9, 10, 11, 12, 13, 14, 15, 16]
[18, 19, 20, 21, 22]。
思路分析:
跟上一题一样的思路,利用两个指针start和end,初始值赋1和2,计算指针之间数值的和,大于s,start后移;小于s,end后移。

public ArrayList> FindContinuousSequence(int sum) {
    ArrayList> ret = new ArrayList<>();
    int start = 1, end = 2;
int curSum = 3;
//small到(1+s)/2时即可结束循环
    while (end < sum) {
        if (curSum > sum) {
            curSum -= start;
            start++;
        } else if (curSum < sum) {
            end++;
            curSum += end;
        } else {
            //找到一组序列存放到list中
            ArrayList list = new ArrayList<>();
            for (int i = start; i <= end; i++)
                list.add(i);
            ret.add(list);
            //start指针后移,寻找下一个满足条件的序列
            curSum -= start;
            start++;
            end++;
            curSum += end;
        }
    }
    return ret;
}

57.1 翻转单词的顺序

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

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

正确的解法应该是和书上一样,先旋转每个单词,再旋转整个字符串。
“I am a student.” -----> ”I ma a .tneduts” -----> “student. a am I ”

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;
}

57.2 左旋转字符串

题目描述:
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。
Input:
S=“abcXYZdef”
K=3

Output:
“XYZdefabc”
思路分析:
将字符串分为两部分,一部分是要旋转的字符,一部分是不用旋转的字符。以"abcXYZdef"为例,将两部分字符分别翻转得到"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;
}

58.滑动窗口的最大值

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

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

public ArrayList maxInWindows(int[] num, int size) {
    ArrayList ret = new ArrayList<>();
    if (size > num.length || size < 1)
        return ret;
    PriorityQueue 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;
}

59.n个骰子的点数(*)

题目描述:
把 n 个骰子扔在地上,求点数和为 s 的概率。输入n,打印出s的所有可能的值出现的概率。
思路分析:
骰子一共有6个面,每个面对应1~6之间一个数字,所以n个骰子的点数和的最小值为n,最大值为6n。n个骰子的所有点数的排列数为。所以我们需要先统计出每个点数出现的次数,然后把每个点数出现的次数除以,就能求出每个点数出现的概率。
方法一: 动态规划解法
使用一个二维数组 dp 存储点数出现的次数,其中 dp[i][j] 表示前 i 个骰子产生点数 j 的次数。
空间复杂度:O(N2)

public List> 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 <= pointNum; j++)     /* 使用 i 个骰子最小点数为 i */
            for (int k = 1; k <= face && k <= j; k++)
                dp[i][j] += dp[i - 1][j - k];

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

    return ret;
}

方法二: 动态规划解法+旋转数组
空间复杂度:O(N)

public List> dicesSum(int n) {
    final int face = 6;
    final int pointNum = face * n;
    long[][] dp = new long[2][pointNum + 1];

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

    int flag = 1;        /* 旋转标记 */
    for (int i = 2; i <= n; i++, flag = 1 - flag) {
        for (int j = 0; j <= pointNum; j++)
            dp[flag][j] = 0;       /* 旋转数组清零 */

        for (int j = i; j <= pointNum; j++)
            for (int k = 1; k <= face && k <= j; k++)
                dp[flag][j] += dp[1 - flag][j - k];
    }

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

    return ret;
}

60.扑克牌顺子

题目描述:
从扑克牌中随机抽取5张牌,判断是不是一个顺子。210为数字本身,A为1,JK为11~13,大小王可看做任意数字。
思路分析:
首先把数组排序;然后统计数组中0的个数;最后统计排序后的数组中相邻数字之间的空缺总数,如果空缺数小于等于0的个数,那么数组是连续的,否则是不连续的。

public boolean isContinuous(int[] nums) {

    if (nums.length < 5)
        return false;

    Arrays.sort(nums);

    // 统计0的数量
    int cnt = 0;
    for (int num : nums)
        if (num == 0)
            cnt++;

    // 使用0去补全不连续的顺子
for (int i = cnt; i < nums.length - 1; i++) {
    //如果存在相同的两个数,那么是不连续的
        if (nums[i + 1] == nums[i])
            return false;
        //用0补缺
        cnt -= nums[i + 1] - nums[i] - 1;
}

    return cnt >= 0;
}

61.圆圈中最后剩下的数

题目描述:
让小朋友们围成一个大圈。然后,随机指定一个数 m,让编号为 0 的小朋友开始报数。每次喊到 m-1 的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续 0…m-1 报数 … 这样下去 … 直到剩下最后一个小朋友,可以不用表演。
思路分析:
根据推理可以得到递归公式:
在这里插入图片描述

约瑟夫环,圆圈长度为 n 的解可以看成长度为 n-1 的解再加上报数的长度 m。因为是圆圈,所以最后需要对 n 取余。

public int LastRemaining_Solution(int n, int m) {
    if (n == 0)     /* 特殊输入的处理 */
        return -1;
    if (n == 1)     /* 递归返回条件 */
        return 0;
    return (LastRemaining_Solution(n - 1, m) + m) % n;
}

62.股票的最大利润

题目描述:
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
例如:一只股票在某些时间节点的价格为{9,11,8,5,7,12,16,14},那么在价格为5时买入,并且在价格为16时卖出,可以获得最大利润。
思路分析:
如果把股票的买入价和卖出价两个数字组成一个数对,那么利润就是这个数对的差值,因此,最大利润就是数组中所有数对的最大差值。
使用贪心策略,假设第 i 轮进行卖出操作,买入操作价格应该在 i 之前并且价格最低。

public int maxProfit(int[] prices) {
    if (prices == null || prices.length == 0)
        return 0;
    //soFarMin 表示到当前位置为止的最小值
int soFarMin = prices[0];
    int maxProfit = 0;
    for (int i = 1; i < prices.length; i++) {
        soFarMin = Math.min(soFarMin, prices[i]);
        //最大利润就是“当前价格-当前为止最小值”和之前利润的较大值
        maxProfit = Math.max(maxProfit, prices[i] - soFarMin);
    }
    return maxProfit;
}

63.求1+2+3…+n

题目描述:
要求不能使用乘除法、for、while、if、else、switch、case 等关键字及条件判断语句 A ? B : C。
思路分析:
使用递归解法最重要的是指定返回条件,但是本题无法直接使用 if 语句来指定返回条件。
条件与 && 具有短路原则,即在第一个条件语句为 false 的情况下不会去执行第二个条件语句。利用这一特性,将递归的返回条件取非然后作为 && 的第一个条件语句,递归的主体转换为第二个条件语句,那么当递归的返回条件为 true 的情况下就不会执行递归的主体部分,递归返回。
本题的递归返回条件为 n <= 0,取非后就是 n > 0;递归的主体部分为 sum += Sum_Solution(n - 1),转换为条件语句后就是 (sum += Sum_Solution(n - 1)) > 0。

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

64.不用加减乘除做加法

题目描述:
写一个函数,求两个整数之和,要求不得使用 +、-、*、/ 四则运算符号。
思路分析:
以十进制加法为例,5+17=22。实际上这个计算可以分三步进行:
第一步做各位相加不进位,此时结果是12;
第二步做进位,5+7中有进位,进位值是10;
第三步把前面两个结果加起来12+10=22。也就是最终结果5+17=22。
对于二进制来说:

  • 第一步不考虑进位的情况下,两个二进制相加等于将两个二进制数做异或;
  • 第二步考虑进位,只有1加1时会向前一位产生进位,等于将两个二进制数按位与再左移一位;
  • 第三步将前两个步骤的结果相加。
public int Add(int a, int b) {
    return b == 0 ? a : Add(a ^ b, (a & b) << 1);
}

65.构建乘积数组

题目描述:
给定一个数组 A[0, 1,…, n-1],请构建一个数组 B[0, 1,…, n-1],其中 B 中的元素 B[i]=A[0]A[1]…*A[i-1]A[i+1]…*A[n-1](注意把A[ i ]刨除去了)。要求不能使用除法。
思路分析:
可以将乘法分为两部分,A[0]A[1]A[i-1]和A[i+1]…*A[n-1]。

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;
}

66.把字符串转换成整数

题目描述:
将一个字符串转换成一个整数,字符串不是一个合法的数值则返回 0,要求不能使用字符串转换整数的库函数。
Iuput:
+2147483647
1a33

Output:
2147483647
0
思路分析:

public int StrToInt(String str) {
    if (str == null || str.length() == 0)
        return 0;
    boolean isNegative = str.charAt(0) == '-';
    int ret = 0;
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (i == 0 && (c == '+' || c == '-'))  /* 符号判定 */
            continue;
        if (c < '0' || c > '9')                /* 非法输入 */
            return 0;
        ret = ret * 10 + (c - '0');
    }
    return isNegative ? -ret : ret;
}

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

对于二叉查找树:
剑指offer之JAVA版---史上最全_第19张图片

二叉查找树中,两个节点 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;
}

对于普通二叉树:
剑指offer之JAVA版---史上最全_第20张图片

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

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;
}

68.快速排序算法:

选择一个基准数,将小于基准数的数全部放在基准数的左边,大于基准数的数全部放在基准数的右边。
也就是找两个指针从数组的两边开始遍历,右边指针先走左边指针再走,当右边指针指向的数小于基准数,左边指针指向的数大于基准数时,交换两个指针指向的值,一直到两个指针相遇,将基准数和两指针指向的数交换。
这样以基准数为界,数组分成了两部分,两部分分别递归调用之前的步骤即可,直到无法再分出子序列。
剑指offer之JAVA版---史上最全_第21张图片

package com.juconnect.iotshare;

import java.util.Scanner;

public class test {
    public static void main(String[] args) {

        Scanner sc = new Scanner(System.in);
        String line = sc.nextLine();
        String[] strArr = line.split(",");
        int[] str = new int[strArr.length];
        for(int i=0; i=pivotkey) {  //从后往前找到比key小的放到前面去
                high--;
            }
            swap(L,low,high);
            while(low

69.动态规划算法:

最大价值问题,通用公式;
共有i件物品,第i件物品对应的重量是Wi,对应的价值是Vi,现有一个承重是j的背包,求能放入的最大价值。
① i=0或j=0时, m[i,0] = m[0,j] = 0
② j ③ j>=Wi时, m[i,j] = (m[i-1,j-Wi]+Vi,m[i-1,j])

你可能感兴趣的:(笔经面经刷题系列)