剑指offer解析-上(Java实现)

我的技术博客:www.zhenganwen.topjava

如下题目按照牛客网在线编程排序,全部代码示例代码均已经过牛客网OJ。node

二维数组的查找

题目描述

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

public boolean Find(int target, int [][] arr) {

}
复制代码

解析

暴力方法是遍历一遍二维数组,找到target就返回true,时间复杂度为O(M * N)(对于M行N列的二维数组)。编程

由题可知输入的数据样本具备高度规律性(单独一行的数据来看是有序的,单独一列的数据来看也是有序的),所以考虑可否有一个比较基准在一次的比较中根据有序性淘汰没必要再进行遍历比较的数。有序查找,由此不难联想到二分查找,咱们能够借鉴二分查找的思路,每次选出一个数做为比较基准进而淘汰掉一些没必要比较的数。二分是选取数组的中位数做为比较基准的,所以可以保证每次都淘汰掉二分之一的数,那么此题中有没有这种特性的数呢?咱们不妨举例观察一下:数组

image

不难发现上图中对角线上的数是其所在行和所在列造成的序列的中位数,不妨选取右上角的数做为比较基准,若是不相等,那么咱们能够淘汰掉全部它左边的数或者它全部下面的,好比对于target = 6,由于(0,3)位置上的4 < 6,所以(0,3)位置及其同一行的左边的全部数都小于6所以能够直接淘汰掉,淘汰掉以后问题就变为了从剩下的三行中找target,这与原始问题是类似的,也就是说每一次都选取右上角的数据为比较基准而后淘汰掉一行或一列,直到某一轮被选取的数就是target或者已经淘汰得只剩下一个数的时候就必定能得出结果了,所以时间复杂度为被淘汰掉的行数和列数之和,即O(M + N),通过分析后不难写出以下代码:缓存

public boolean Find(int target, int [][] arr) {
    //input check
    if(arr == null || arr.length == 0 || arr[0] == null || arr[0].length == 0){
        return false;
    }
    int i = 0, j = arr[0].length - 1;
    while(i != arr.length - 1 && j != 0){
        if(target > arr[i][j]){
            i++;
        }else if(target < arr[i][j]){
            j--;
        }else{
            return true;
        }
    }

    return target == arr[i][j];
}
复制代码

值得注意的是每次选取的数都是第一行最后一个数,所以前提是第一行有数,那么就对应着输入检查的arr[0] == null || arr[0].length == 0,这点比较容易忽略。数据结构

总结:通过分析其实不难发现,此题是在一维有序数组使用二分查找元素的一个变种,咱们应该充分利用数据自己的规律性来寻找解题思路。app

替换空格

题目描述

请实现一个函数,将一个字符串中的每一个空格替换成“%20”。例如,当字符串为We Are Happy.则通过替换以后的字符串为We%20Are%20Happy。dom

public String replaceSpace(StringBuffer str) {
    
}
复制代码

此题考查的是字符串这个数据结构的数组实现(对应的还有链表实现)的相关操做。函数

解析

String.replace简单粗暴

若是可使用API,那么能够很容易地写出以下代码:

public String replaceSpace(StringBuffer str) {
    //input check
    //null pointer
    if(str == null){
        return null;
    }
    //empty str or not exist blank
    if(str.length() == 0 || str.indexOf(" ") == -1){
        return str.toString();
    }

    for(int i = 0 ; i < str.length() ; i++){
        if(str.charAt(i) == ' '){
            str.replace(i, i + 1, "%20");
        }
    }

    return str.toString();
}
复制代码
时间O(n),空间O(n)

可是若是面试官告诉咱们不准使用封装好的替换函数,那么目的就是在考查咱们对字符串数组实现方式的相关操做。因为是连续空间存储,所以须要在建立实例时指定大小,因为每一个空格都使用%20替换,所以替换以后的字符串应该比原串多出空格数 * 2个长度,实现以下:

public String replaceSpace(StringBuffer str) {
    //input check
    //null pointer
    if(str == null){
        return null;
    }
    //empty str or not exist blank
    if(str.length() == 0 || str.indexOf(" ") == -1){
        return str.toString();
    }

    char[] source = str.toString().toCharArray();
    int blankCount = 0;
    for(int i = 0 ; i < source.length ; i++){
        blankCount = (source[i] == ' ') ? blankCount + 1 : blankCount;
    }
    char[] dest = new char[source.length + blankCount * 2];
    for(int i = source.length - 1, j = dest.length - 1 ; i >=0 && j >=0 ; i--, j--){
        if(source[i] == ' '){
            dest[j--] = '0';
            dest[j--] = '2';
            dest[j] = '%';
            continue;
        }else{
            dest[j] = source[i];
        }
    }

    return new String(dest);
}
复制代码
时间O(n),空间O(1)

若是还要求不能有额外空间,那咱们就要考虑如何复用输入的字符串,若是咱们从前日后遇到空格就将空格及其以后的两个位置替换为%20,势必会覆盖空格以后的两个字符,好比hello world会被替换成hello%20rld,所以咱们须要在长度被扩展后的新串中从后往前肯定每一个索引上的字符。好比使用一个originalIndex指向原串中的最后一个字符索引,使用newIndex指向新串的最后一个索引,每次将originalIndex上的字符复制到newIndex上而且两个指针前移,若是originalIndex上的字符是空格,则将newIndex依次填充0,2,%,而后二者再前移,直到二者都到首索引位置。

image

public String replaceSpace(StringBuffer str) {
    //input check
    //null pointer
    if(str == null){
        return null;
    }
    //empty str or not exist blank
    if(str.length() == 0 || str.indexOf(" ") == -1){
        return str.toString();
    }

    int blankCount = 0;
    for(int i = 0 ; i < str.length() ; i++){
        blankCount = (str.charAt(i) == ' ') ? blankCount + 1 : blankCount;
    }
    int originalIndex = str.length() - 1, newIndex = str.length() - 1 + blankCount * 2;
    str.setLength(newIndex + 1); //须要从新设置一下字符串的长度,不然会报越界错误
    while(originalIndex >= 0 && newIndex >= 0){
        if(str.charAt(originalIndex) == ' '){
            str.setCharAt(newIndex--, '0');
            str.setCharAt(newIndex--, '2');
            str.setCharAt(newIndex, '%');
        }else{
            str.setCharAt(newIndex, str.charAt(originalIndex));
        }
        originalIndex--;
        newIndex--;
    }

    return str.toString();
}
复制代码

总结:要把思惟打开,对于数组的操做咱们习惯性的以for(int i = 0 ; i < arr.length ; i++)的形式从头至尾来操做数组,可是不要忽略了从尾到头遍历也有它的独到之处。

反转链表

题目描述

输入一个链表,反转链表后,输出新链表的表头。

public ListNode ReverseList(ListNode head) {
        
}
复制代码

解析

此题的难点在于没法经过一个单链表结点获取其前驱结点,所以咱们不只要在反转指针以前保存当前结点的前驱结点,还要保存当前结点的后继结点,并在下一次反转以前更新这两个指针。

/* 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 pre = null, p = head, next;
    while(p != null){
        next = p.next;
        p.next = pre;
        pre = p;
        p = next;
    }

    return pre;
}
复制代码

从尾到头打印链表

题目描述

输入一个链表,按链表值从尾到头的顺序返回一个ArrayList。

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
      
}
复制代码

解析

此题的难点在于单链表只有指向后继结点的指针,所以咱们没法经过当前结点获取前驱结点,所以不要妄想先遍历一遍链表找到尾结点而后再依次从后往前打印。

递归,简洁优雅

因为咱们一般是从头至尾遍历链表的,而题目要求从尾到头打印结点,这与前进后出的逻辑是相符的,所以你可使用一个栈来保存遍历时走过的结点,再经过后进先出的特性实现从尾到头打印结点,可是咱们也能够利用递归来帮咱们压栈,因为递归简洁不易出错,所以面试中能用递归尽可能用递归:只要当前结点不为空,就递归遍历后继结点,当后继结点为空时,递归结束,在递归回溯时将“当前结点”依次添加到集合中

/** * public class ListNode { * int val; * ListNode next = null; * * ListNode(int val) { * this.val = val; * } * } * */
import java.util.ArrayList;
public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        ArrayList<Integer> res = new ArrayList();
        //input check
        if(listNode == null){
            return res;
        }
        recursively(res, listNode);
        return res;
    }

    public void recursively(ArrayList<Integer> res, ListNode node){
        //base case
        if(node == null){
            return;
        }
        //node not null
        recursively(res, node.next);
        res.add(node.val);
        return;
    }
}
复制代码
反转链表

还有一种方法就是将链表指针都反转,这样将反转后的链表从头至尾打印就是结果了。须要注意的是咱们不该该在访问用户数据时更改存储数据的结构,所以最后要记得反转回来:

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ArrayList<Integer> res = new ArrayList();
    //input check
    if(listNode == null){
        return res;
    }
    return unrecursively(listNode);
}

public ArrayList<Integer> unrecursively(ListNode node){
    ArrayList<Integer> res = new ArrayList<Integer>();
    ListNode newHead = reverse(node);
    ListNode p = newHead;
    while(p != null){
        res.add(p.val);
        p = p.next;
    }
    reverse(newHead);
    return res;
}

public ListNode reverse(ListNode node){
    ListNode pre = null, cur = node, next;
    while(cur != null){
        //save predecessor
        next = cur.next;
        //reverse pointer
        cur.next = pre;
        //move to next
        pre = cur;
        cur = next;
    }
    //cur is null
    return pre;
}
复制代码

总结:面试时能用递归就用递归,固然了若是面试官就是要考查你的指针功底那你也能just so so不是

重建二叉树

题目描述

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,2,7,1,5,3,8,6},则重建二叉树并返回。

public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
   
}
复制代码

解析

先序序列的特色是第一个数就是根结点然后是左子树的先序序列和右子树的先序序列,而中序序列的特色是先是左子树的中序序列,而后是根结点,最后是右子树的中序序列。所以咱们能够经过先序序列获得根结点,而后经过在中序序列中查找根结点的索引从而获得左子树和右子树的结点数。而后能够将两序列都一分为三,对于其中的根结点可以直接重建,而后根据对应子序列分别递归重建根结点的左子树和右子树。这是一个典型的将复杂问题划分红子问题分步解决的过程。

image

递归体的定义,如上图先序序列的左子树序列是2,3,4对应下标1,2,3,而中序序列的左子树序列是3,2,4对应下标0,1,2,所以递归体接收的参数除了保存两个序列的数组以外,还须要指明须要递归重建的子序列分别在两个数组中的索引范围:TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n)。而后递归体根据prei~j索引范围造成的先序序列和inm~n索引范围造成的中序序列重建一棵树并返回根结点。

首先根结点就是先序序列的第一个数,即pre[i],所以TreeNode root = new TreeNode(pre[i])能够直接肯定,而后经过在inm~n中查找出pre[i]的索引index能够求得左子树结点数leftNodes = index - m,右子树结点数rightNodes = n - index,若是左(右)子树结点数为0则代表左(右)子树为null,不然经过root.left = rebuild(pre, i' ,j' ,in ,m' ,n')来重建左(右)子树便可。

这个题的难点也就在这里,即i',j',m',n'的值的肯定,笔者曾在此困惑许久,建议经过leftNodes,rightNodesi,j,m,n来肯定:(这个时候了前往不要在脑子里面想这些下标对应关系!!必定要在纸上画,确保准确性和归纳性)

image

因而容易得出以下代码:

if(leftNodes == 0){
    root.left = null;
}else{
    root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1);
}
if(rightNodes == 0){
    root.right = null;
}else{
    root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n);
}
复制代码

笔者曾以中序序列的根节点索引来肯定i',j',m',n'的对应关系写出以下错误代码

image

if(leftNodes == 0){
    root.left = null;
}else{
    root.left = rebuild(pre, i + 1, index, in, m, index - 1);
}
if(rightNodes == 0){
    root.right = null;
}else{
    root.right = rebuild(pre, index + 1, j, in, index + 1, n);
}
复制代码

这种对应关系乍一看没错,可是不具备归纳性(即囊括全部状况),好比对序列2,3,43,2,4重建时:

image

你看这种状况,上述错误代码还适用吗?缘由就在于index是在inm~n中选取的,与数组in是绑定的,和pre没有直接的关系,所以若是用index来表示i',j'天然是不合理的。

此题的正确完整代码以下:

/** * 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) {
        if(pre == null || in == null || pre.length == 0 || in.length == 0 || pre.length != in.length){
            return null;
        }
        return rebuild(pre, 0, pre.length - 1, in, 0, in.length - 1);
    }
    
    public TreeNode rebuild(int[] pre, int i, int j, int[] in, int m, int n){
        int rootVal = pre[i], index = findIndex(rootVal, in, m, n);
        if(index < 0){
            return null;
        }
        int leftNodes = index - m, rightNodes = n - index;
        TreeNode root = new TreeNode(rootVal);
        if(leftNodes == 0){
            root.left = null;
        }else{
            root.left = rebuild(pre, i + 1, i + leftNodes, in, m, m + leftNodes - 1);
        }
        if(rightNodes == 0){
            root.right = null;
        }else{
            root.right = rebuild(pre, i + leftNodes + 1, j, in, n - rightNodes + 1, n);
        }
        return root;
    }
    
    public int findIndex(int target, int arr[], int from, int to){
        for(int i = from ; i <= to ; i++){
            if(arr[i] == target){
                return i;
            }
        }
        return -1;
    }
}
复制代码

总结:

  1. 对于复杂问题,必定要划分红若干子问题,逐一求解。好比二叉树问题,咱们一般将其划分红头结点、左子树、右子树。
  2. 对于递归过程的参数对应关系,尽可能使用和数据样本自己没有直接关系的变量来表示。好比此题应该选取leftNodesrightNodes来计算i',j',m',n'而不该该使用头结点在中序序列的下标index(它和in是绑定的,那么可能对pre就不适用了)。

用两个栈实现队列

题目描述

用两个栈来实现一个队列,完成队列的Push和Pop操做。 队列中的元素为int类型。

Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();

public void push(int node) {

}

public int pop() {
    
}
复制代码

解析

这道题只要记住如下几点便可:

  1. 一个栈(如stack1)只能用来存,另外一个栈(如stack2)只能用来取
  2. 当取元素时首先检查stack2是否为空,若是不空直接stack2.pop(),不然将stack1中的元素所有倒入stack2,若是倒入以后stack2仍为空则须要抛异常,不然stack2.pop()

代码示例以下:

import java.util.Stack;

public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    
    public void push(int node) {
        stack1.push(node);
    }
    
    public int pop() {
        if(stack2.empty()){
            while(!stack1.empty()){
                stack2.push(stack1.pop());
            }
        }
        if(stack2.empty()){
            throw new IllegalStateException("no more element!");
        }
        return stack2.pop();
    }
}
复制代码

总结:只要取元素的栈不为空,取元素时直接弹出其栈顶元素便可,只有当其为空时才考虑将存元素的栈倒入进来,而且要一次性倒完。

旋转数组的最小数字

题目描述

把一个数组最开始的若干个元素搬到数组的末尾,咱们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的全部元素都大于0,若数组大小为0,请返回0。

public int minNumberInRotateArray(int [] arr) {
       
}
复制代码

解析

此题需先认真审题:

  1. 若干,涵盖了一个元素都不搬的状况,此时数组是一个非减排序序列,所以首元素就是数组的最小元素。
  2. 非减排序,并不表明是递增的,可能会出现若干相邻元素相同的状况,极端的例子是整个数组的全部元素都相同

由此不可贵出以下input check

public int minNumberInRotateArray(int [] arr) {
    //input check
    if(arr == null || arr.length == 0){
        return 0;
    }
    //if only one element or no rotate
    if(arr.length == 1 || arr[0] < arr[arr.length - 1]){
        return arr[0];
    }
    
    //TODO
}
复制代码

上述的arr[0] < arr[arr.length - 1]不能写成arr[0] <= arr[arr.length - 1],好比可能会有[1,2,3,3,4] -> [3,4,1,2,3] 的状况,这时你不能返回arr[0]=3

若是走到了程序中的TODO,就能够考虑广泛状况下的推敲,数组能够被分红两部分:大于等于arr[0]的左半部分和小于等于arr[arr.length - 1]右半部分,咱们不妨借助两个指针从数组的头、尾向中间靠近,这样就能利用二分的思想快速移动指针从而淘汰一些不在考虑范围以内的数。

image

如图,咱们不能直接经过arr[mid]arr[l](或arr[r])的比较(arr[mid] >= arr[l])来决定移动l仍是rmid上,由于数组可能存在若干相同且相邻的数,所以咱们还须要加上一个限制条件:arr[l + 1] >= arr[l] && arr[mid] >= arr[l](对于r来讲则是arr[r - 1] <= arr[r] && arr[mid] <= arr[r]),即当左半部分(右半部分)不止一个数时,咱们才可能去移动lr)指针。完整代码以下:

import java.util.ArrayList;
public class Solution {
    public int minNumberInRotateArray(int [] arr) {
         //input check
        if(arr == null || arr.length == 0){
            return 0;
        }
        //if only one element or no rotate
        if(arr.length == 1 || arr[0] < arr[arr.length - 1]){
            return arr[0];
        }
         
        //has rotate, left part is big than right part
        int l = 0, r = arr.length - 1, mid;
        //l~r has more than 3 elements
        while(r > l && r - l != 1){
            //r-l >= 2 -> mid > l
            mid = l + ((r - l) >> 1);
            if(arr[l + 1] >= arr[l] && arr[mid] >= arr[l]){
                l = mid;
            }else{
                r = mid;
            }
        }
         
        return arr[r];
    }
}
复制代码

总结:审题时要充分考虑数据样本的极端状况,以写出鲁棒性较强的代码。

斐波那契数列

题目描述

你们都知道斐波那契数列,如今要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。n<=39

public int Fibonacci(int n) {
      
}
复制代码

解析

递归方式

对于公式f(n) = f(n-1) + f(n-2),明显就是一个递归调用,所以根据f(0) = 0f(1) = 1咱们不难写出以下代码:

public int Fibonacci(int n) {
    if(n == 0 || n == 1){
        return n;
    }
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}
复制代码
动态规划

在上述递归过程当中,你会发现有不少计算过程是重复的:

image

动态规划就在使用递归调用自上而下分析过程当中发现有不少重复计算的子过程,因而采用自下而上的方式将每一个子状态缓存下来,这样对于上层而言只有当须要的子过程结果不在缓存中时才会计算一次,所以每一个子过程都只会被计算一次

public int Fibonacci(int n) {
    if(n == 0 || n == 1){
        return n;
    }
    //n1 -> f(n-1), n2 -> f(n-2)
    int n1 = 1, n2 = 0;
    //从f(2)开始算起
    int N = 2, res = 0;
    while(N++ <= n){
        //每次计算后更新缓存,固然你也可使用一个一维数组保存每次的计算结果,只额外空间复杂度就变为O(n)了
        res = n1 + n2;
        n2 = n1;
        n1 = res;
    }
    return res;
}
复制代码

上述代码不少人都能写出来,只是没有意识到这就是动态规划。

总结:当你自上而下分析递归时发现有不少子过程被重复计算,那么就应该考虑可否经过自下而上将每一个子过程的计算结果缓存下来。

跳台阶

题目描述

一只青蛙一次能够跳上1级台阶,也能够跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(前后次序不一样算不一样的结果)。

public int JumpFloor(int target) {
       
}
复制代码

解析

递归版本

将复杂问题分解:复杂问题就是不断地将target减1或减2(对应跳一级和跳两级台阶)直到target变为1或2(对应只剩下一层或两层台阶)时咱们可以很容易地得出结果。所以对于当前的青蛙而言,它可以选择的就是跳一级或跳二级,剩下的台阶有多少种跳法交给子过程来解决:

public int JumpFloor(int target) {
    //input check
    if(target <= 0){
        return 0;
    }
    //base case
    if(target == 1){
        return 1;
    }
    if(target == 2){
        return 2;
    }
    return JumpFloor(target - 1) + JumpFloor(target - 2);
}
复制代码

你会发现这其实就是一个斐波那契数列,只不过是从f(1) = 1,f(2) = 2开始的斐波那契数列罢了。天然你也应该可以写出动态规划版本。

进阶问题

一只青蛙一次能够跳上1级台阶,也能够跳上2级……它也能够跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

解析

递归版本

本质上仍是分解,只不过上一个是分解成两步,而这个是分解成n步:

public int JumpFloorII(int target) {
    if(target <= 0){
        return 0;
    }
    //base case,当target=0时表示某个分解分支跳完了全部台阶,这个分支就是一种跳法
    if(target == 0){
        return 1;
    }
    
    //本过程要收集的跳法的总数
    int res = 0;
    for(int i = 1 ; i <= target ; i++){
         //本次选择,选择跳i阶台阶,剩下的台阶交给子过程,每一个选择就表明一个分解分支
        res += JumpFloorII(target - i);
    }
    return res;
}
复制代码
动态规划

这个动态规划就有一点难度了,首先咱们要肯定缓存目标,斐波那契数列中因为f(n)只依赖于f(n-1)f(n-2)所以咱们仅用两个缓存变量实现了动态规划,可是这里f(n)依赖的是f(0),f(1),f(2),...,f(n-1),所以咱们须要经过长度量级为n的表缓存前n个状态(int arr[] = new int[target + 1]arr[target]表示f(n))。而后根据递归版本(一般是base case)肯定哪些状态的值是能够直接肯定的,好比由if(target == 0){ return 1 }可知arr[0] = 1,从f(N = 1)开始的全部状态都须要依赖以前(f(n < N))的全部状态:

int res = 0;
for(int i = 1 ; i <= target ; i++){
    res += JumpFloorII(target - i);
}
return res
复制代码

所以咱们能够据此自下而上计算出每一个子状态的值:

public int JumpFloorII(int target) {
    if(target <= 0){
        return 0;
    }

    int arr[] = new int[target + 1];
    arr[0] = 1;
    for(int i = 1 ; i < arr.length ; i++){
        for(int j = 0 ; j < i ; j++){
            arr[i] += arr[j];
        }
    }

    return arr[target];
}
复制代码

但这仍不是最优解,由于观察循环体你会发现,每次f(n)的计算都要从f(0)累加到f(n-1),咱们彻底能够将这个累加值缓存起来preSum,每计算出一次f(N)以后都将缓存更新为preSum += f(N)。如此获得最优解:

public int JumpFloorII(int target) {
    if(target <= 0){
        return 0;
    }

    int arr[] = new int[target + 1];
    arr[0] = 1;
    int preSum = arr[0];
    for(int i = 1 ; i < arr.length ; i++){
        arr[i] = preSum;
        preSum += arr[i];
    }

    return arr[target];
}
复制代码

矩形覆盖

题目描述

咱们能够用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

public int RectCover(int target) {
        
}
复制代码

解析

递归版本

有了以前的历练,咱们能很快的写出递归版本:先竖着放一个或者先横着放两个,剩下的交给递归处理:

//target 大矩形的边长,也是剩余小矩形的个数
public int RectCover(int target) {
    if(target <= 0){
        return 0;
    }
    if(target == 1 || target == 2){
        return target;
    }
    return RectCover(target - 1) + RectCover(target - 2);
}
复制代码
动态规划

这仍然是个以f(1)=1,f(2)=2开头的斐波那契数列:

//target 大矩形的边长,也是剩余小矩形的个数
public int RectCover(int target) {
    if(target <= 0){
        return 0;
    }
    if(target == 1 || target == 2){
        return target;
    }
    //n_1->f(n-1), n_2->f(n-2),从f(N=3)开始算起
    int n_1 = 2, n_2 = 1, N = 3, res = 0;
    while(N++ <= target){
        res = n_1 + n_2;
        n_2 = n_1;
        n_1 = res;
    }

    return res;
}
复制代码

二进制中1的个数

题目描述

输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。

public int NumberOf1(int n) {
       
}
复制代码

解析

题目已经给咱们下降了难度:负数用补码(取反加1)表示代表输入的参数为均为正数,咱们只需统计其二进制表示中1的个数、运算时只考虑无符号移位便可。

典型的判断某个二进制位上是否为1的方法是将该二进制数右移至该二进制位为最低位而后与1相与&,因为1的二进制表示中只有最低位为1其他位均为0,所以相与后的结果与该二进制位上的数相同。据此不难写出以下代码:

public int NumberOf1(int n) {
    int count = 0;
    for(int i = 0 ; i < 32 ; i++){
        count += ((n >> i) & 1);
    }
    return count;
}
复制代码

固然了,还有一种比较秀的解法就是利用n = n & (n - 1)n的二进制位中为1的最低位置为0(只要n不为0就说明含有二进位制为1的位,如此这样的操做能作多少次就说明有多少个二进制位为1的位):

public int NumberOf1(int n) {
    int count = 0;
    while(n != 0){
        count++;
        n &= (n - 1);
    }
    return count;
}
复制代码

数值的整数次方

题目描述

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

public double Power(double base, int exponent) {
        
}
复制代码

解析

这是一道充满危险色彩的题,求职者可能会心里窃喜不假思索的写出以下代码:

public double Power(double base, int exponent) {
    double res = 1;
    for(int i = 1 ; i <= exponent ; i++){
        res *= base;
    }
	return res;
}
复制代码

可是你有没有想过底数base和幂exponent都是可正、可负、可为0的。若是幂为负数,那么底数就不能为0,不然应该抛出算术异常:

//是不是负数
boolean minus = false;
//若是存在分母
if(exponent < 0){
    minus = true;
    exponent = -exponent;
    if(base == 0){
        throw new ArithmeticException("/ by zero");
    }
}
复制代码

若是幂为0,那么根据任何不为0的数的0次方为1,0的0次方未定义,应该有以下判断:

//若是指数为0
if(exponent == 0){
    if(base != 0){
        return 1;
    }else{
        throw new ArithmeticException("0^0 is undefined");
    }
}
复制代码

剩下的就是计算乘方结果,可是不要忘了若是幂为负须要将结果取倒数:

//指数不为0且分母也不为0,正常计算并返回整数或分数
double res = 1;
for(int i = 1 ; i <= exponent ; i++){
    res *= base;
}

if(minus){
    return 1/res;
}else{
    return res;
}
复制代码

也许你还能够锦上添花为幂乘方的计算引入二分计算(当幂为偶数时2^n = 2^(n/2) * 2^(n/2)):

public double binaryPower(double base, int exp){
    if(exp == 1){
        return base;
    }
    double res = 1;
    res *= (binaryPower(base, exp/2) * binaryPower(base, exp/2));
    return exp % 2 == 0 ? res : res * base;
}
复制代码

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

题目描述

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得全部的奇数位于数组的前半部分,全部的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变

public void reOrderArray(int [] arr) {
      
}
复制代码

解析

读题以后发现这个跟快排的partition思路很像,都是选取一个比较基准将数组分红两部分,固然你也能够以arr[i] % 2 == 0为基准将奇数放前半部分,将偶数放有半部分,可是虽然只需O(n)的时间复杂度但不能保证调整后奇数之间、偶数之间的相对位置:

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

    int odd = -1;
    for(int i = 0 ; i < arr.length ; i++){
        if(arr[i] % 2 == 1){
            swap(arr, ++odd, i);
        }
    }
}

public void swap(int[] arr, int i, int j){
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
复制代码

涉及到排序稳定性,咱们天然可以想到插入排序,从数组的第二个元素开始向后依次肯定每一个元素应处的位置,肯定的逻辑是:将该数与前一个数比较,若是比前一个数小则与前一个数交换位置并在交换位置后继续与前一个数比较直到前一个数小于等于该数或者已达数组首部中止。

此题不过是将比较的逻辑由数值的大小改成:当前的数是不是奇数而且前一个数是偶数,是则递归向前交换位置。代码示例以下:

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

    int odd = -1;
    for(int i = 1 ; i < arr.length ; i++){
        for(int j = i ; j >= 1 ; j--){
            if(arr[j] % 2 == 1 && arr[j - 1] % 2 == 0){
                swap(arr, j, j - 1);
            }
        }
    }
}
复制代码

链表中倒数第K个结点

题目描述

输入一个链表,输出该链表中倒数第k个结点。

public ListNode FindKthToTail(ListNode head,int k) {
    
}
复制代码

解析

倒数,这又是一个从尾到头的遍历逻辑,而链表对从尾到头遍历是敏感的,前面咱们有经过压栈/递归、反转链表的方式实现这个遍历逻辑,天然对于此题一样适用,可是那样未免太麻烦了,咱们能够经过两个间距为(k-1)个结点的链表指针来达到此目的。

public ListNode FindKthToTail(ListNode head,int k) {
    //input check
    if(head == null || k <= 0){
        return null;
    }
    ListNode tmp = new ListNode(0);
    tmp.next = head;
    ListNode p1 = tmp, p2 = tmp;
    while(k > 0 && p1.next != null){
        p1 = p1.next;
        k--;
    }
    //length < k
    if(k != 0){
        return null;
    }
    while(p1 != null){
        p1 = p1.next;
        p2 = p2.next;
    }
    
    tmp = null; //help gc

    return p2;
}
复制代码

这里使用了一个技巧,就是建立一个临时结点tmp做为两个指针的初始指向,以模拟p1先走k步以后,p2才开始走,没走时停留在初始位置的逻辑,有利于帮咱们梳理指针在对应位置上的意义,这样当p1走到头时(p1=null),p2就是倒数第k个结点。

这里还有一个坑就是,笔者层试图为了简化代码将上述的9 ~ 12行写成以下偷懒模式而致使排错许久:

while(k-- > 0 && p1.next != null){
        p1 = p1.next;
}
复制代码

缘由是将k--写在while()中,不管判断是否经过都会执行k = k - 1,所以代码老是会在if(k != 0)处返回null,但愿读者不要和笔者同样粗心。

总结:当遇到复杂的指针操做时,咱们不妨试图多引入几个指针或者临时结点,以方便梳理咱们的思路,增强代码的逻辑化,这些空间复杂度O(1)的操做一般也不会影响性能。

合并两个排序的链表

题目描述

输入两个单调递增的链表,输出两个链表合成后的链表,固然咱们须要合成后的链表知足单调不减规则。

public ListNode Merge(ListNode list1,ListNode list2) {
    
}
复制代码

解析

image

public ListNode Merge(ListNode list1,ListNode list2) {
    if(list1 == null || list2 == null){
        return list1 == null ? list2 : list1;
    }
    ListNode newHead = list1.val < list2.val ? list1 : list2;
    ListNode p1 = (newHead == list1) ? list1.next : list1;
    ListNode p2 = (newHead == list2) ? list2.next : list2;
    ListNode p = newHead;
    while(p1 != null && p2 != null){
        if(p1.val <= p2.val){
            p.next = p1;
            p1 = p1.next;
        }else{
            p.next = p2;
            p2 = p2.next;
        }
        p = p.next;
    }

    while(p1 != null){
        p.next = p1;
        p = p.next;
        p1 = p1.next;
    }
    while(p2 != null){
        p.next = p2;
        p = p.next;
        p2 = p2.next;
    }

    return newHead;
}
复制代码

树的子结构

题目描述

输入两棵二叉树A,B,判断B是否是A的子结构。(ps:咱们约定空树不是任意一个树的子结构)

/** public class TreeNode { int val = 0; TreeNode left = null; TreeNode right = null; public TreeNode(int val) { this.val = val; } }*/
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
    if(root1 == null || root2 == null){
        return false;
    }

    return process(root1, root2);
}
复制代码

解析

这是一道典型的分解求解的复杂问题。典型的二叉树分解:遍历头结点、遍历左子树、遍历右子树。首先按照root1root2的值是否相等划分为两种状况:

  1. 两个头结点的值相等,而且root2.left也是roo1.left的子结构(递归)、root2.right也是root1.right的子结构(递归),那么可返回true
  2. 不然,要看只有当root2root1.left的子结构或者root2root1.right的子结构时,才能返回true

据上述两点很容易得出以下递归逻辑:

if(root1.val == root2.val){
    if(process(root1.left, root2.left) && process(root1.right, root2.right)){
        return true;
    }
}

return process(root1.left, root2) || process(root1.right, root2);
复制代码

接下来肯定递归的终止条件,若是某个子过程root2=null那么说明在自上而下的比较过程当中root2的结点已被罗列比较完了,这时不管root1是否为null,该子过程都应该返回true

image

if(root2 == null){
    return true;
}
复制代码

可是若是root2 != nullroot1 = null,则应返回false

image

if(root1 == null && root2 != null){
    return false;
} 
复制代码

完整代码以下:

public class Solution {
    public boolean HasSubtree(TreeNode root1,TreeNode root2) {
        if(root1 == null || root2 == null){
            return false;
        }

        return process(root1, root2);
    }

    public boolean process(TreeNode root1, TreeNode root2){
        if(root2 == null){
            return true;
        }
        if(root1 == null && root2 != null){
            return false;
        }  

        if(root1.val == root2.val){
            if(process(root1.left, root2.left) && process(root1.right, root2.right)){
                return true;
            }
        }

        return process(root1.left, root2) || process(root1.right, root2);
    }
}
复制代码

二叉树的镜像

题目描述

操做给定的二叉树,将其变换为源二叉树的镜像。

image

public void Mirror(TreeNode root) {
        
}
复制代码

解析

由图可知获取二叉树的镜像就是将原树的每一个结点的左右孩子交换一下位置(这个规律必定要会找),也就是说咱们只需遍历每一个结点并交换left,right的引用指向就能够了,而咱们有成熟的先序遍历:

public void Mirror(TreeNode root) {
    if(root == null){
        return;
    }

    TreeNode tmp = root.left;
    root.left = root.right;
    root.right = tmp;
    Mirror(root.left);
    Mirror(root.right);
}
复制代码

顺时针打印矩阵

题目描述

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每个数字,例如,若是输入以下4 X 4矩阵: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 则依次打印出数字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.

public ArrayList<Integer> printMatrix(int [][] matrix) {
        
}
复制代码

解析

image

只要分析清楚了打印思路(左上角和右下角便可肯定一条打印轨迹)后,此题主要考查条件控制的把握。只要给我一个左上角的点(i,j)和右下角的点(m,n),就能够将这一圈的打印分解为四步:

image

可是若是左上角和右下角的点在一行或一列上那就不必分解,直接打印改行或该列便可,打印的逻辑以下:

public void printEdge(int[][] matrix, int i, int j, int m, int n, ArrayList<Integer> res){
    if(i == m && j == n){
        res.add(matrix[i][j]);
        return;
    }

    if(i == m || j == n){
        //only one while will be execute
        while(i < m){
            res.add(matrix[i++][j]);
        }
        while(j < n){
            res.add(matrix[i][j++]);
        }
        res.add(matrix[m][n]);
        return;
    }

    int p = i, q = j;
    while(q < n){
        res.add(matrix[p][q++]);
    }
    //q == n
    while(p < m){
        res.add(matrix[p++][q]);
    }
    //p == m
    while(q > j){
        res.add(matrix[p][q--]);
    }
    //q == j
    while(p > i){
        res.add(matrix[p--][q]);
    }
    //p == i
}
复制代码

接着咱们将每一个圈的左上角和右下角传入该函数便可:

public ArrayList<Integer> printMatrix(int [][] matrix) {
    ArrayList<Integer> res = new ArrayList<Integer>();
    if(matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0){
        return res;
    }
    int i = 0, j = 0, m = matrix.length - 1, n = matrix[0].length - 1;
    while(i <= m && j <= n){
        printEdge(matrix, i++, j++, m--, n--, res);
    }
    return res;
}
复制代码

包含min函数的栈

题目描述

定义栈的数据结构,请在该类型中实现一个可以获得栈中所含最小元素的min函数(时间复杂度应为O(1))。

public class Solution {

    
    public void push(int node) {
        
    }
    
    public void pop() {
        
    }
    
    public int top() {
        
    }
    
    public int min() {
        
    }
}
复制代码

解析

最直接的思路是使用一个变量保存栈中现有元素的最小值,但这只对只存不取的栈有效,当弹出的值不是最小值时还没什么影响,但当弹出最小值后咱们就没法获取当前栈中的最小值。解决思路是使用一个最小值栈,栈顶老是保存当前栈中的最小值,每次数据栈存入数据时最小值栈就要相应的将存入后的最小值压入栈顶:

private Stack<Integer> dataStack = new Stack();
private Stack<Integer> minStack = new Stack();

public void push(int node) {
    dataStack.push(node);
    if(!minStack.empty() && minStack.peek() < node){
        minStack.push(minStack.peek());
    }else{
        minStack.push(node);
    }
}

public void pop() {
    if(!dataStack.empty()){
        dataStack.pop();
        minStack.pop();
    }
}

public int top() {
    if(!dataStack.empty()){
        return dataStack.peek();
    }
    throw new IllegalStateException("stack is empty");
}

public int min() {
    if(!dataStack.empty()){
        return minStack.peek();
    }
    throw new IllegalStateException("stack is empty");
}
复制代码

栈的压入、弹出序列

题目描述

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的全部数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不多是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)

public boolean IsPopOrder(int [] arr1,int [] arr2) {
     
}
复制代码

解析

可使用两个指针i,j,初始时i指向压入序列的第一个,j指向弹出序列的第一个,试图将压入序列按照顺序压入栈中:

  1. 若是arr1[i] != arr2[j],那么将arr1[i]压入栈中并后移i(表示arr1[i]还没到该它弹出的时刻)
  2. 若是某次后移i以后发现arr1[i] == arr2[j],那么说明此刻的arr1[i]被压入后应该被当即弹出才会产生给定的弹出序列,因而不压入arr1[i](表示压入并弹出了)并后移ij也要后移(表示弹出序列的arr2[j]记录已产生,接着产生或许的弹出记录便可)。
  3. 由于步骤2和3都会后移i,所以循环的终止条件是i到达arr1.length,此时若栈中还有元素,那么从栈顶到栈底造成的序列必须与arr2j以后的序列相同才能返回true
public boolean IsPopOrder(int [] arr1,int [] arr2) {
    //input check
    if(arr1 == null || arr2 == null || arr1.length != arr2.length || arr1.length == 0){
        return false;
    }
    Stack<Integer> stack = new Stack();
    int length = arr1.length;
    int i = 0, j = 0;
    while(i < length && j < length){
        if(arr1[i] != arr2[j]){
            stack.push(arr1[i++]);
        }else{
            i++;
            j++;
        }
    }

    while(j < length){
        if(arr2[j] != stack.peek()){
            return false;
        }else{
            stack.pop();
            j++;
        }
    }

    return stack.empty() && j == length;
}
复制代码

从上往下打印二叉树

题目描述

从上往下打印出二叉树的每一个节点,同层节点从左至右打印。

public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
       
}
复制代码

解析

使用一个队列来保存当前遍历结点的孩子结点,首先将根节点加入队列中,而后进行队列非空循环:

  1. 从队列头取出一个结点,将该结点的值打印
  2. 若是取出的结点左孩子不空,则将其左孩子放入队列尾部
  3. 若是取出的结点右孩子不空,则将其右孩子放入队列尾部
public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
    ArrayList<Integer> res = new ArrayList<Integer>();
    if(root == null){
        return res;
    }
    LinkedList<TreeNode> queue = new LinkedList();
    queue.addLast(root);
    while(queue.size() > 0){
        TreeNode node = queue.pollFirst();
        res.add(node.val);
        if(node.left != null){
            queue.addLast(node.left);
        }
        if(node.right != null){
            queue.addLast(node.right);
        }
    }

    return res;
}
复制代码

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

题目描述

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

public boolean VerifySquenceOfBST(int [] sequence) {
        
}
复制代码

解析

对于二叉树的后序序列,咱们可以肯定最后一个数就是根结点,还能肯定的是前一半部分是左子树的后序序列,后一部分是右子树的后序序列。

遇到这种复杂问题,咱们仍能采用三步走战略(根结点、左子树、右子树):

  1. 若是当前根结点的左子树是BST且其右子树也是BST,那么才多是BST
  2. 在1的条件下,若是左子树的最大值小于根结点且右子树的最小值大于根结点,那么这棵树就是BST

据此咱们须要定义一个递归体,该递归体须要收集的信息以下:下层须要向我返回其最大值、最小值、以及是不是BST

class Info{
    boolean isBST;
    int max;
    int min;
    Info(boolean isBST, int max, int min){
        this.isBST = isBST;
        this.max = max;
        this.min = min;
    }
}
复制代码

递归体的定义以下:

public Info process(int[] arr, int start, int end){
    if(start < 0 || end > arr.length - 1 || start > end){
        throw new IllegalArgumentException("invalid input");
    }
    //base case : only one node
    if(start == end){
        return new Info(true, arr[end], arr[end]);
    }

    int root = arr[end];
    Info left, right;
    //not exist left child
    if(arr[start] > root){
        right = process(arr, start, end - 1);
        return new Info(root < right.min && right.isBST, 
                        Math.max(root, right.max), Math.min(root, right.min));
    }
    //not exist right child
    if(arr[end - 1] < root){
        left = process(arr, start, end - 1);
        return new Info(root > left.max && left.isBST, 
                        Math.max(root, left.max), Math.min(root, left.min));
    }

    int l = 0, r = end - 1;
    while(r > l && r - l != 1){
        int mid = l + ((r - l) >> 1);
        if(arr[mid] > root){
            r = mid;
        }else{
            l = mid;
        }
    }
    left = process(arr, start, l);
    right = process(arr, r, end - 1);
    return new Info(left.isBST && right.isBST && root > left.max && root < right.min, 
                    right.max, left.min);
}
复制代码

总结:二叉树相关的信息收集问题分步走:

  1. 分析当前状态须要收集的信息
  2. 根据下层传来的信息加工出当前状态的信息
  3. 肯定递归终止条件

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

题目描述

输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的全部路径。路径定义为从树的根结点开始往下一直到叶结点所通过的结点造成一条路径。(注意: 在返回值的list中,数组长度大的数组靠前)

public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
        
}
复制代码

解析

审题可知,咱们须要有一个自上而下从根结点到每一个叶子结点的遍历思路,而先序遍历恰好能够拿来用,咱们只需在来到当前结点时将当前结点值加入到栈中,在离开当前结点时再将栈中保存的当前结点的值弹出便可使用栈模拟保存自上而下通过的结点,从而实如今来到每一个叶子结点时只需判断栈中数值之和是否为target便可。

public ArrayList<ArrayList<Integer>> FindPath(TreeNode root,int target) {
    ArrayList<ArrayList<Integer>> res = new ArrayList();
    if(root == null){
        return res;
    }
    Stack<Integer> stack = new Stack<Integer>();
    preOrder(root, stack, 0, target, res);
    return res;
}

public void preOrder(TreeNode root, Stack<Integer> stack, int sum, int target, ArrayList<ArrayList<Integer>> res){
    if(root == null){
        return;
    }

    stack.push(root.val);
    sum += root.val;
    //leaf node
    if(root.left == null && root.right == null && sum == target){
        ArrayList<Integer> one = new ArrayList();
        one.addAll(stack);
        res.add(one);
    }

    preOrder(root.left, stack, sum, target, res);
    preOrder(root.right, stack, sum, target, res);

    sum -= stack.pop();
}
复制代码

复杂链表的复制

题目描述

输入一个复杂链表(每一个节点中有节点值,以及两个指针,一个指向下一个节点,另外一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的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) {
        
    }
}
复制代码

解析

此题主要的难点在于random指针的处理。

方法一:使用哈希表,额外空间O(n)

能够将链表中的结点都复制一份,用一个哈希表来保存,key是源结点,value就是副本结点,而后遍历key取出每一个对应的value将副本结点的next指针和random指针设置好:

public RandomListNode Clone(RandomListNode pHead){
    if(pHead == null){
        return null;
    }
    HashMap<RandomListNode, RandomListNode> map = new HashMap();
    RandomListNode p = pHead;
    //copy
    while(p != null){
        RandomListNode cp = new RandomListNode(p.label);
        map.put(p, cp);
        p = p.next;
    }
    //link
    p = pHead;
    while(p != null){
        RandomListNode cp = map.get(p);
        cp.next = (p.next == null) ? null : map.get(p.next);
        cp.random = (p.random == null) ? null : map.get(p.random);
        p = p.next;
    }

    return map.get(pHead);
}
复制代码
方法二:追加结点,额外空间O(1)

首先将每一个结点复制一份并插入到对应结点以后,而后遍历链表将副本结点的random指针设置好,最后将源结点和副本结点分离成两个链表

public RandomListNode Clone(RandomListNode pHead){
    if(pHead == null){
        return null;
    }

    RandomListNode p = pHead;
    while(p != null){
        RandomListNode cp = new RandomListNode(p.label);
        cp.next = p.next;
        p.next = cp;
        p = p.next.next;
    }

    //more than two node
    //link random pointer
    p = pHead;
    RandomListNode cp;
    while(p != null){
        cp = p.next;
        cp.random = (p.random == null) ? null : p.random.next;
        p = p.next.next;
    }

    //split source and copy
    p = pHead;
    RandomListNode newHead = p.next;
    //p != null -> p.next != null
    while(p != null){
        cp = p.next;
        p.next = p.next.next;
        p = p.next;
        cp.next = (p == null) ? null : p.next;
    }

    return newHead;
}
复制代码
相关文章
相关标签/搜索