《剑指offer 第二版》题解

剑指Offer

按题号排序


3. 数组中重复的数字

题目一:找出数组中重复的数字

在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是重复的数字2或3。

→ 牛客网在线评判

题目详解

解法1:

思路:先将数组排序,再从头到尾扫描排序后的数组。

时间复杂度:O(nlogn) 空间复杂度:O(1)

import java.util.Arrays;

public class Main {
    public boolean duplicate(int numbers[],int length,int [] duplication) {
        // 当数组为空和数组长度小于等于零,直接返回
        if (numbers == null || length <= 0) {
            return false;
        }
        // 当数组所有元素都小于零或大于n-1,直接返回
        for (int i = 0 ; i < length; i++) {
            if (numbers[i] > length - 1 || numbers[i] < 0) return false;
        }
        // 对数组排序
        Arrays.sort(numbers);
        // 查找相同元素
        for (int i = 0; i < length - 1 ; i++) {
            if(numbers[i] == numbers[i+1]){
                duplication[0] = numbers[i];
                return true;
            }
        }
        return false;
     }
}
解法2:

思路:利用哈希表key值唯一特性

时间复杂度:O(n)

空间复杂度:O(n)

import java.util.HashSet;

public class Main {
    public boolean duplicate(int numbers[],int length,int [] duplication) {
        // 当数组为空和数组长度小于等于零,直接返回
        if (numbers == null || length <= 0) {
            return false;
        }
        // 当数组所有元素都小于零或大于n-1,直接返回
        for (int i = 0 ; i < length; i++) {
            if (numbers[i] > length - 1 || numbers[i] < 0) return false;
        }
        // 新建HashSet
        HashSet hashSet = new HashSet();
        for (int i = 0; i < length; i++) {
            // 判断hashSet中是否含有相同数据,若有返回true,没有将数据添加到hashSet中
            if (hashSet.contains(numbers[i])) {
                duplication[0] = numbers[i];
                return true;
            }else {
                hashSet.add(numbers[i]);
            }
        }
        return false;
     }
}
解法3(推荐):

思路:从头到尾扫描数组,扫描到下标为i的数字,首先比较数字(用m表示)是不是等于i。如果是,扫描下一个数字;如果不是,将它和第m个数字进行对比。如果相等,则为重复数字;如果不相等,则进行交换。

时间复杂度:O(n)

空间复杂度:O(1)

public class Main {
    public boolean duplicate(int numbers[],int length,int [] duplication) {
        // 当数组为空和数组长度小于等于零,直接返回
        if (numbers == null || length <= 0) {
            return false;
        }
        // 当数组所有元素都小于零或大于n-1,直接返回
        for (int i = 0 ; i < length; i++) {
            if (numbers[i] > length - 1 || numbers[i] < 0) return false;
        }
        //重排数组
        for (int i = 0; i < length; i++) {
            while (numbers[i] != i) {
                if (numbers[i] == numbers[numbers[i]]){
                    duplication[0] = numbers[i];
                    return true;
                }
                int tmp = numbers[i];
                numbers[i] = numbers[tmp];
                numbers[tmp] = tmp;
            }
        }
        return false;
     }

题目二:不修改数组找出重复的数字

在一个长度为n+1的数组里的所有数字都在1到n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为8的数组{2,3,5,4,3,2,6,7},那么对应的输出是重复数字2或3。

题目详解

解法1(空间换时间):

思路:创建一个n+1长的辅助数组,依次添加数据到其中,若数据值为m,则添加到对应下标为m的位置,若此位置已经有数据则说明m为重复数据。

时间复杂度:O(n)

空间复杂度:O(n)

public class Main {
    public int getDuplication(int[] numbers, int length) {
        // 当数组为空和数组长度小于等于零,直接返回
        if (numbers == null || length <= 0){
            return -1;
        }
        // 创建长度为n+1的辅助数组
        int[] tmp = new int[length];
        // 遍历数组找出重复数字
        for (int i = 0; i <length ; i++) {
            if (numbers[i] == tmp[numbers[i]]) {
                return numbers[i];
            } else {
                tmp[numbers[i]] = numbers[i];
            }
        }
        return -1;
    }
}
解法2(时间换空间):

思路:将1到n的数字从中间数字m分为两部分,前一半为1到m,后一半为m+1到n。如果1到m的数字的数目超过m,则这一半中一定含有重复数字;否则另一半包含重复数字。继续将包含重复数字的区间一份为2,直到找到重复数字。类似于二分查找。

时间复杂度:O(nlogn)

空间复杂度:O(1)

public class Main {
    public int getDuplication(int[] numbers, int length) {
        // 当数组为空和数组长度小于等于零,直接返回
        if (numbers == null || length <= 0){
            return -1;
        }
        // 开始折半查找
        int start = 1;
        int end = length - 1;
        while (end >= start){
            // 计算一半的大小
            int middle = (end - start)/2 +start;
            int count = count(numbers,length,start,middle);
            // 如果起始位置相同,且数目大于1,则为重复数字
            if (end == start){
                if (count>1)
                    return start;
                else
                    break;
            }
            //如果前一半数据多,将middle作为end继续查找;反之,将middle+1作为start
            if (count > (middle-start+1))
                end = middle;
            else
                start = middle + 1;
        }
        return -1;
    }
    // 计算numbers中start到middle范围个数
    private int count(int[] numbers,int length, int start, int middle) {
        if(numbers == null) return 0;
        int count = 0;
        for (int i = 0; i <length ; i++) {
            if (numbers[i] >= start && numbers[i] <= middle) count++;
        }
        return count;
    }
}

4. 二维数组中的查找

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:首先选取数组中右上角的数字。如果该数字等于要查找数字,则查找过程结束;如果大于查找数字,则剔除这个数字所在列;如果小于查找数字,则剔除所在行。依次剔除直到完成查找。 时间复杂度:O(n) 空间复杂度:O(1)

public class Main {
    public boolean Find(int target, int [][] array) {
        // 若数组为空,返回false
        if(array == null)
            return false;
        // 若数组行或列长度为0,返回false
        if(array.length == 0 || array[0].length==0)
            return false;
        // 计算二位数组行列数
        int row = array.length;
        int col = array[0].length;
        // 从数组右上角开始查找
        int i = 0, j = col -1;
        while (i < row && j >= 0) {
            // 如果和目标相等,返回true
            // 如果大于目标,剔除所在列
            // 如果小于目标,剔除所在行
            if (target == array[i][j]){
                return true;
            } else if (array[i][j] > target){
                j--;
            }else {
                i++;
            }
        }
        // 未找到
        return false;
    }
}

5. 替换空格

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:先遍历一次字符串,根据字符串中的空格数增加字符串的长度(一个空格增加长度2)。从字符串的后面开始复制和替换。利用两个索引index1和index2,将index1指向原始字符串末尾,index2指向新字符串末尾。向前移动index1,逐个将指向的字符复制到index2指向位置,直到碰到空格。碰到空格时,index1向前移动1位,index2之前插入"%20",向前移动3位。依次运行,直到所有字符替换完毕。 时间复杂度:O(n) 空间复杂度:O(1)

public class Main {
    public String replaceSpace(StringBuffer str) {
        // 计算原始字符串长度
        int originalLength = str.length();
        // 如果为null或者为空串,直接返回
        if (str == null || originalLength <= 0) return str.toString();
        // 查询空格数,并在末尾添加两个空格占位
        for (int i = 0; i <originalLength ; i++) {
            if (str.charAt(i) == ' ')
                str.append("  ");
        }
        // 计算新字符串长度
        int newLength = str.length();
        // 如果字符串长度无变化,表示字符串中无空格,直接返回
        if (newLength == originalLength) return  str.toString();
        // 从末尾开始对比,如果为空格则替换为"%20",不是则直接复制
        int indexOfOriginal = originalLength - 1;
        int indexOfNew = newLength - 1;
        while (indexOfOriginal>=0 && indexOfNew>indexOfOriginal){
            if (str.charAt(indexOfOriginal)==' '){
                str.setCharAt(indexOfNew--,'0');
                str.setCharAt(indexOfNew--,'2');
                str.setCharAt(indexOfNew--,'%');
            }else {
                str.setCharAt(indexOfNew--,str.charAt(indexOfOriginal));
            }
            indexOfOriginal--;
        }
        return str.toString();
    }
}

6. 从头到尾打印链表

题目:

输入一个链表,从尾到头打印链表每个节点的值。

→ 牛客网在线评判
//链表结构
public class ListNode {
    int val;
    ListNode next = null;
    ListNode(int val) {
        this.val = val;
    }
}

题目详解

解法1(栈):

思路:利用栈的“后进先出”,首先遍历链表并将数据存入栈中,之后再将数据依次pop出。 时间复杂度:O(n) 空间复杂度:O(n)

public class Main {
    public String replaceSpace(StringBuffer str) {
        // 计算原始字符串长度
        int originalLength = str.length();
        // 如果为null或者为空串,直接返回
        if (str == null || originalLength <= 0) return str.toString();
        // 查询空格数,并在末尾添加两个空格占位
        for (int i = 0; i <originalLength ; i++) {
            if (str.charAt(i) == ' ')
                str.append("  ");
        }
        // 计算新字符串长度
        int newLength = str.length();
        // 如果字符串长度无变化,表示字符串中无空格,直接返回
        if (newLength == originalLength) return  str.toString();
        // 从末尾开始对比,如果为空格则替换为"%20",不是则直接复制
        int indexOfOriginal = originalLength - 1;
        int indexOfNew = newLength - 1;
        while (indexOfOriginal>=0 && indexOfNew>indexOfOriginal){
            if (str.charAt(indexOfOriginal)==' '){
                str.setCharAt(indexOfNew--,'0');
                str.setCharAt(indexOfNew--,'2');
                str.setCharAt(indexOfNew--,'%');
            }else {
                str.setCharAt(indexOfNew--,str.charAt(indexOfOriginal));
            }
            indexOfOriginal--;
        }
        return str.toString();
    }
}
解法2(递归):

思路:递归本质上就是一个栈结构。先递归输出它后面的节点,再输出节点本身。 时间复杂度:O(n) 空间复杂度:O(n)

import java.util.ArrayList;

public class Main {
        public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
            // 新建列表用于存放结果
            ArrayList<Integer> list = new ArrayList<>();
            // 如果节点不为空
            if (listNode != null) {
                // 如果存在下一个节点
                if (listNode.next != null){
                    // 将后续节点值加入list中
                    list = printListFromTailToHead(listNode.next);
                }
                // 将当前节点值加入list
                list.add(listNode.val);
            }
            // 返回链表
            return list;
        }
}

7. 重建二叉树

题目:

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

→ 牛客网在线评判
//二叉树节点
public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { val = x; }
}

题目详解

解法(递归):
思路:前序遍历的第一个数字就是根节点值。扫描中序遍历,就能确定根节点的位置。根据中序遍历的特点,根节点前面的数字是左子树节点,后面的数字是右子树节点。再对子树递归使用算法即可重建二叉树。
public class Main {
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        // 前序和中序不存在或长度为零
        if (pre == null || in == null || pre.length <= 0
                || in.length <= 0)
            return null;
        int length = pre.length;
        // 重建二叉树
        return reConstruct(pre,in,0,length,0,length);
    }

    private TreeNode reConstruct(int[] pre, int[] in, int startPre, int endPre, int startIn, int endIn) {
        // 获取根节点值并新建节点
        int rootValue = pre[startPre];
        TreeNode root = new TreeNode(rootValue);
        // 如果子树不存在子节点,返回当前节点
        if (startPre == endPre){
            if (pre[startPre] == in[startIn] && startIn == endIn) return root;
            else return null;
        }
        // 查找根节点位置
        int indexIn = 0;
        for (int i = startIn; i <endIn ; i++) {
            if (in[i] == rootValue) indexIn = i;
        }
        // 计算左子树节点数
        int leftLength = indexIn - startIn;
        // 重建左子树
        if (leftLength > 0){
            root.left = reConstruct(pre,in,startPre+1,startPre+leftLength+1,startIn,indexIn);
        }
        // 重建右子树
        if(leftLength<(endPre - startPre - 1)){
            root.right = reConstruct(pre,in,startPre+leftLength+1,endPre,indexIn+1,endIn);
        }
        // 返回重建后子树
        return root;
    }
}

8. 二叉树的下一个节点

题目:

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

→ 牛客网在线评判
//二叉树节点
public class TreeLinkNode {
    int val;
    TreeLinkNode left = null;
    TreeLinkNode right = null;
    TreeLinkNode next = null;

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

题目详解

解法:

思路:如果当前节点有右节点,则下一个节点为右子树最左边的节点。如果没有右子树,且为父节点的左子节点,则父节点为下一个节点;若为父节点的右子节点,则向上查找,直到查找节点为父节点的左子节点时,对应父节点即为下一个,若不存在这样的节点,当前节点即为最后一个节点。

public class Main {
    public TreeLinkNode GetNext(TreeLinkNode pNode) {
        // 若为空节点
        if (pNode == null) {
            return null;
        }
        TreeLinkNode nextNode = null;
        // 若有右子树
        if (pNode.right != null){
            nextNode = pNode.right;
            while (nextNode.left != null)
                nextNode = nextNode.left;
        }else if(pNode.next != null){ // 若无右子树且不为根节点
            // 若为左节点,父节点即为下一个
            if (pNode.next.left == pNode)
                nextNode = pNode.next;
            // 若为右节点,向上查找,直到节点为父节点的左子节点时,对应父节点即为下一个
            else if (pNode.next.right == pNode) {
                // 若不存在这样的节点,当前节点即为最后一个
                while (pNode.next != null && pNode.next.right == pNode) {
                    pNode = pNode.next;
                }
                nextNode = pNode.next;
            }
        }
        return nextNode;
    }
}

9. 用两个栈实现队列

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:将操作分为插入元素和删除元素两种。对于插入操作,直接将数据添加到stack1中。对于删除元素,如果stack2中不含有元素,则先将stack1中的元素全部放入stack2中,再删除栈顶元素;如果stack2中含有元素,直接删除栈顶即可。

import java.util.Stack;

public class Main {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    // 插入元素,直接将数据添加到stack1中
    public void push(int node) {
        stack1.push(node);
    }
    // 删除元素,如果stack2为空,先将stack1中数据全部放入stack2中,再pop
    // 如果stack2不为空,直接pop
    public int pop() {
        if (stack2.empty() )
            while (!stack1.empty())
                stack2.push(stack1.pop());
        return stack2.pop();
    }
}

10. 斐波那契数列

题目一:

输入n,求斐波那契(Fibonacci)数列的第n项。

F[n]=F[n-1]+F[n-2](n>2,F[0]=0,F[1]=1)
→ 牛客网在线评判

题目详解

解法1(不推荐):

思路:直接利用函数递归实现。但由于计算了很多重复数据,计算量会随着n增大而急剧增大;算法复杂度也以n的指数方式递增。

public class Main {
    public int Fibonacci(int n) {
        // 计算F(0)
        if (n<=0) return 0;
        // 计算F(1)
        if (n == 1) return 1;
        //计算F(n - 1) + F(n - 2)
        return Fibonacci(n - 1) + Fibonacci(n - 2);
    }
}
解法2(推荐):

思路:利用循环从下往上计算F(n)。首先根据F(1)和F(2)算出F(3),再更具F(2)和F(3)算出F(4)...以此类推直到F(n) 时间复杂度:O(n)

public class Main {
    public int Fibonacci(int n) {
        // 当n为1或0时直接返回
        if (n < 2) return n;
        // 将F(n-1)初始化为1,F(n-2)初始化为0
        int fibNsubOne = 1;
        int fibNsubTwo = 0;
        int result = 0;
        // 利用循环计算F(n)
        for (int i = 2; i <= n; i++) {
            // 计算F(i)
            result = fibNsubOne + fibNsubTwo;
            // 每次循环将当前F(i-1)的值赋给F(i-2)
            fibNsubTwo = fibNsubOne;
            // 将当前F(i)的值赋给F(i-1),用作下一次计算
            fibNsubOne = result;
        }
        return result;
    }
}

题目二:

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

→ 牛客网在线评判

题目详解

解法:

思路:将n级台阶看成n的函数,记为F(n)。当n>2时,就有两种不同的选择:一次是第一次跳1级,此时跳法数目等于后面剩下n-1级的跳法数目,即F(n-1);二是第一次跳2级,此时跳法数目等于后面剩下n-2级的跳法数目,即F(n-2)。因此,n级台阶的不同跳法总数为F(n) = F(n - 1) + F(n - 2),实际上就是斐波那契数列。 时间复杂度:O(n)

public class Main {
    public int JumpFloor(int target) {
        // 当只有2级以下台阶时
        if (target <= 2) return target;
        // 若台阶大于2级,F(n) = F(n - 1) + F(n - 2)
        // F(2) = 2,F(1) = 1
        int jumpOne = 2;
        int jumpTwo = 1;
        int result = 0;
        // 利用循环计算跳n次台阶跳法;
        for (int i = 3; i <= target; i++) {
            result = jumpOne + jumpTwo;
            jumpTwo = jumpOne;
            jumpOne = result;
        }
        return result;
    }
}

11. 旋转数组中最小数字

题目:

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

→ 牛客网在线评判

题目详解

解法(二分查找):

思路:利用二分查找思想进行查找。首先获取中点索引以及对应数组元素值。如果中点索引对应元素与前后索引对应元素相等时,则使用顺序查找;若不相等,当中间元素大于前索引指向元素时,说明最小元素在后半部,将中点索引值赋给前索引,继续查找;当中间元素小于后索引指向元素时,说明最小元素在前半部,将中点索引值赋给后索引,继续查找。当前后索引值相差为1时结束查找。 时间复杂度:O(logn)

public class Main {
    public int minNumberInRotateArray(int [] array) {
        int length = array.length;
        // 若数组为空或长度为0,返回0
        if (array == null || length <= 0) return 0;
        // 设置查找索引
        int index1 = 0;
        int index2 = length - 1;
        int indexMid = 0;
        // 当第一个索引指向元素大于第二个时继续查找
        while (array[index1] >= array[index2]) {
            // 当索引值相差为1时结束查找
            if ((index2 - index1) == 1) {
                indexMid = index2;
                break;
            }
            // 获取中点索引值
            indexMid = (index1 + index2)/2;
            // 当中点索引对应元素与前后对应元素相等时,使用顺序查找
            if (array[indexMid] == array[index1] && array[indexMid] == array[index2])
                return minInOrder(array,index1,index2);
            // 当中间元素大于前索引指向元素时,说明最小元素在后半部,将中点索引值赋给前索引
            if (array[indexMid] >= array[index1])
                index1 = indexMid;
            // 当中间元素小于后索引指向元素时,说明最小元素在前半部,将中点索引值赋给后索引
            if (array[indexMid] <= array[index2])
                index2 = indexMid;
        }
        return array[indexMid];
    }
    // 顺序查找最小值
    private int minInOrder(int[] array, int index1, int index2) {
        int result = array[index1];
        for (int i = index1+1; i < index2; i++) {
            if (result > array[i])
                result = array[i];
        }
        return result;
    }
}

12. 矩阵中的路径

题目:

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

→ 牛客网在线评判

题目详解

解法(回溯法):

思路:首先,在矩阵中任选一个格子作为路径的起点。假设矩阵中某个格子的字符为ch,并且这个格子将对应于路径上第i个字符。如果路径上第i个字符不是ch,那么这个格子不是第i个位置。如果路径上第i个字符正好是ch,则到相邻的格子寻找路径上第i+1个字符。除矩阵边界上的格子之外,其他格子都有四个相邻的格子。重复这个过程,直到路径上所有字符都在矩阵中找到。

public class Main {
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
        int length = matrix.length;
        // 排除特殊情况
        if (matrix == null || rows <= 0 || cols<=0 || str == null || length != (rows * cols))
            return false;
        // 设置查找索引
        int pathLength = 0;
        // 设置访问数组,记录已经访问过的数据
        boolean[] visited = new boolean[rows*cols];
        // 遍历数组查找起点
        for (int i = 0; i < rows; i++)
            for (int j = 0; j < cols; j++)
                // 回溯法实现路径查找
                if (hasThatPath(matrix,rows,cols,i,j,str,pathLength,visited))
                    return true;
        return false;
    }
    // 路径查找核心代码
    private boolean hasThatPath(char[] matrix, int rows, int cols, int row, int col, 
                                char[] str, int pathLength, boolean[] visited) {
        // 已查找完所有字母时,返回true
        if (pathLength == str.length)
            return true;
        boolean hasPath = false;
        // 1)验证当前位置的坐标值,保证对应数组中含有此元素。
        // 2)验证当前位置元素值与待查找元素是否一致
        // 3)验证当前元素是否已被访问
        if (row >= 0 && row <rows && col >= 0 && col < cols &&
                matrix[row*cols+col] == str[pathLength] && !visited[row*cols+col]){
            pathLength++;
            visited[row*cols+col] = true;
            // 验证当前位置相邻元素是否存在满足条件的情况
            hasPath = hasThatPath(matrix,rows,cols,row - 1,col,str,pathLength,visited) ||
                    hasThatPath(matrix,rows,cols,row + 1,col,str,pathLength,visited) ||
                    hasThatPath(matrix,rows,cols,row,col - 1,str,pathLength,visited) ||
                    hasThatPath(matrix,rows,cols,row,col + 1,str,pathLength,visited);
            // 若无满足条件的元素,则进行回溯
            if (!hasPath){
                pathLength--;
                visited[row*cols+col] = false;
            }
        }
        return hasPath;
    }
}

13. 机器人的运动范围

题目:

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

→ 牛客网在线评判

题目详解

解法(回溯法):

思路:本题依旧使用回溯法,基本思想和12题差不多。机器人从坐标(0,0)开始移动。当它准备进入坐标为(row,col)的格子时,通过检查坐标的数位来判断时候能够进入。如果可以进入,再判断相邻的格子是否能够进入。以此查询直到找出全部可以进入的格子。

public class Main {
    public int movingCount(int threshold, int rows, int cols) {
        // 排除特殊情况
        if(threshold < 0 || rows<=0 || cols <= 0)
            return 0;
        // 创建访问数组
        boolean[] visited = new boolean[rows*cols];
        // 利用回溯法求格子数
        int count = movingCountCore(threshold,rows,cols,0,0,visited);
        return count;
    }
    // 核心算法
    private int movingCountCore(int threshold, int rows, int cols, int row, int col, boolean[] visited) {
        int count = 0;
        // 判断当前格子是否满足条件
        if (check(threshold,rows,cols,row,col,visited)){
            visited[row*cols + col] = true;
            // 检查格子上下左右格子是否满足条件
            count = 1 + movingCountCore(threshold,rows,cols,row-1,col,visited)
                    + movingCountCore(threshold,rows,cols,row+1,col,visited)
                    + movingCountCore(threshold,rows,cols,row,col-1,visited)
                    + movingCountCore(threshold,rows,cols,row,col+1,visited);
        }
        // 返回格子数
        return count;
    }
    // 条件判断方法
    private boolean check(int threshold, int rows, int cols, int row, int col, boolean[] visited) {
        if (row >=0 && row < rows && col >= 0 && col < cols
                && (getDigSum(row)+getDigSum(col)) <= threshold && !visited[row*cols + col])
            return true;
        return false;
    }
    // 计算数位之和方法
    private int getDigSum(int num) {
        int sum = 0;
        while(num > 0){
            sum += num % 10;
            num /= 10;
        }
        return sum;
    }
}

14. 剪绳子

题目:

给你一根长度为n的绳子,请把绳子剪成m段(m、n都是整数,n>1且m>1),每根绳子的长度记为k[0],k[1],...,k[m]。请问k[0]k[1]...*k[m]可能的最大乘积是多少?例如,当绳子长度为8时,剪成长度为2、3、3的三段,此时得到最大乘积18。

题目详解

解法1(动态规划):

思路:首先定义函数f(n)为把长度为n的绳子剪成若干段后各段乘积的最大值。对于剪第一刀,我们有n-1种可能性。因此可知 f(n) = max(f(i)Xf(n-i)),其中0<i<n。这是一个至上而下的递归公式,但由于递归会有很多重复的子问题。更好的办法是从下而上顺序计算,即先得到f(2)、f(3),再计算f(4)、f(5),直到f(n)。 时间复杂度:O(n^2) 空间复杂度:O(n)

public class Main {
    public  int maxProductAfterCutting(int length){
        // 当绳子长度为0到3时,剪后乘积会小于本身长度
        if (length < 2)
            return 0;
        if (length == 2)
            return 1;
        if (length == 3)
            return 2;
        int[] products = new int[length+1];
        // 设置0到3剪后乘积为绳子本身长度
        products[0] = 0;
        products[1] = 1;
        products[2] = 2;
        products[3] = 3;
        int max;
        for (int i = 4; i <= length ; i++) {
            max = 0;
            // 计算products[i]的最大值
            for (int j = 1; j <= i/2 ; j++) {
                int product = products[j]*products[i-j];
                if (max < product)
                    max = product;
                products[i] = max;
            }
        }
        // 返回长度为length的最大乘积
        max = products[length];
        return max;
    }
}
解法2(贪婪算法):

思路:当n>=5时,可以得到>n且3(n-3)>n。即当绳子剩下的长度大于或者等于5的时候,我们把它剪成长度为3或者2的绳子段。另外,当n>=5时,3(n-3)>2(n-2),因此我们应该尽可能地多剪长度为3的绳子。当绳子长度为4的时候,由2X2>1X3知,剪成两段长度为2的更好。 时间复杂度:O(1) 空间复杂度:O(1)

public  int maxProductAfterCutting(int length){
        // 当绳子长度为0到3时,剪后乘积会小于本身长度
        if (length < 2)
            return 0;
        if (length == 2)
            return 1;
        if (length == 3)
            return 2;
        // 尽可能剪去长度为3的绳子
        int timesOf3 = length/3;
        // 当剩下长度为4时,剪成长度为2的两段,2*2 > 1*3
        if ((length - timesOf3*3)==1)
            timesOf3 -= 1;
        int timesOf2 = (length - timesOf3*3)/2;
        // 返回最大乘积
        return (int) (Math.pow(3,timesOf3) * Math.pow(2,timesOf2));
    }

15. 二进制中1的个数

题目:

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

→ 牛客网在线评判

题目详解

解法1:

思路:利用Integer的bitCount()方法可以直接得到个数。

public class Main {
    public int NumberOf1(int n) {
        return Integer.bitCount(n);
    }
}
解法2:

思路:利用2进制的特点。将一个不为0的数减去1时,如果最后一位是1,减去1后,最后一位变成0而其他所有位都保持不变,相当于最后一位做了取反操作;如果最后一位不是1,如果二进制数最右边的第一个1位于m位,那么减去1时,第m位由1变为0,而m位之后的所有0变为1,m位之前的位保持不变。由此可知,把一个整数减去1,再和原来整数做与运算,会把改整数最右边的1变成0。一个整数的二进制有多少个1,就进行多少次操作即可。

public class Main {
    public int NumberOf1(int n) {
        int count = 0;
        while (n!=0){
            count++;
            n = (n-1) & n;
        }
        return count;
    }
}

16. 数值的整数次方

题目:

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

→ 牛客网在线评判

题目详解

解法1(不够高效):

思路:考虑到基数为0,指数为正数,负数和零的情况。当基数等于0且指数小于0时,计算没有意义。当指数为负数时,先计算指数的绝对值,根据绝对值求得相应次方值,再取倒数即为所求。

public class Main {
    public double Power(double base, int exponent) {
        Double base1 = base;
        // 当基数等于0且指数小于0时,不存在结果
        if (base1.equals(0.0) && exponent < 0)
            return 0;
        int absExponent = Math.abs(exponent);
        // 求指数绝对值对应的次方大小
        double result = PowerWithAbsExponent(base,absExponent);
        // 如果指数为负数,则结果为倒数
        if (exponent < 0)
            result = 1.0/result;
        return result;
    }
    // 指数为正数情况下次方结果
    private double PowerWithAbsExponent(double base, int absExponent) {
        double result = 1;
        for (int i = 1; i <= absExponent ; i++) {
            result *= base;
        }
        return result;
    }
}
解法2(高效):

思路:上述求次方的计算不够高效。通过如下公式,可以提高计算效率,再配合位运算效率大于除法和取模,可以进一步优化算法。

a^n = \begin{cases} a^{n/2} * a^{n/2}  & \text {n为偶数} \\ a^{(n-1)/2} * a^{(n-1)/2} & \text{n为奇数} \end{cases} 
public class Main {
    public double Power(double base, int exponent) {
        Double base1 = base;
        // 当基数等于0且指数小于0时,不存在结果
        if (base1.equals(0.0) && exponent < 0)
            return 0;
        int absExponent = Math.abs(exponent);
        // 求指数绝对值对应的次方大小
        double result = PowerWithAbsExponent(base,absExponent);
        // 如果指数为负数,则结果为倒数
        if (exponent < 0)
            result = 1.0/result;
        return result;
    }
    // 指数为正数情况下次方结果
    private double PowerWithAbsExponent(double base, int absExponent) {
       if (absExponent == 0)
           return 1;
       if (absExponent == 1)
           return base;
        // 利用公式高效计算base^absExponent的值
       double result = PowerWithAbsExponent(base,absExponent >> 1);
       result *= result;
       // 如果指数为奇数,则还需乘一个base
       if ((absExponent & 0x1) == 1)
           result *= base;
       return result;
    }
}

17. 打印从1到最大的n位数

题目:

输入数字n,按顺序打印出从1到最大的n位十进制数。比如输入3,则打印1、2、3直到最大的三位数999。

题目详解

解法1:

思路:题目中未指定n的大小,可能需要考虑大数情况,通常使用字符串或者数组来表示大数。使用字符串表示时,字符串长度为n,每个字符都是'0'到'9'之间的某一个字符。首先初始化每一个字符为'0',然后每次每一次为字符串表示的数字加1,再打印出来。而对于判断终止条件,可以对当前数作加一操作,如果产生了最高位进位,则说明溢出,当前数字为最后一个。对于打印数字,要删除最高位之前为0的字符再打印。

public class Main {
    public void print1ToN(int n) {
        // 排除特殊情况
        if (n<=0)
            return;
        StringBuilder number = new StringBuilder();
        for (int i = 0; i <n ; i++) {
            number.append(0);
        }
        // 判断是否为最后一个数字
        while (!increment(number)){
            printNumber(number);
        }
    }
    // 判断当前数字是否为最后一个
    private boolean increment(StringBuilder number) {
        boolean isOverflow = false;
        // 设置进位量
        int nTakeOver = 0;
        int nLength = number.length();
        // 对当前数字加1,判断是否为最后一个
        for (int i = nLength-1; i >=0 ; i--) {
            int sum =number.charAt(i) - '0' + nTakeOver;
           // 对最低位数字加1
            if (i == nLength -1)
                sum++;
            // 如果产生进位,再对次低位依次进行判断
            if (sum >= 10) {
                // 如果是最高位进位,则溢出
                if (i == 0) {
                    isOverflow = true;
                } else {
                    sum -= 10;
                    nTakeOver = 1;
                    number.setCharAt(i, (char) (sum+'0'));
                }
            // 如果未产生进位,则肯定不为最后一个
            }else {
                number.setCharAt(i, (char) (sum+'0'));
                break;
            }
        }
        return isOverflow;
    }
    // 打印数字(剔除高位0)
    private void printNumber(StringBuilder number) {
        boolean isBegin0 = true;
        int nLength = number.length();
        // 剔除占位的0字符,打印出实际数字
        for (int i = 0; i < nLength; i++) {
            if (isBegin0 && number.charAt(i) != '0')
                isBegin0 = false;
            if (!isBegin0){
                System.out.print(number.charAt(i));
            }
        }
        System.out.print("\t");
    }
}
解法2(递归):

思路:n位所有十进制数其实就是n个从0到9的全排列。即把数字的每一位都从0到9全排列一遍,就得到所有的十进制数。

public class Main {
    public void print1ToN(int n) {
        // 排除特殊情况
        if (n<=0)
            return;
        StringBuilder number = new StringBuilder();
        number.setLength(n);
        // 设置最高位的全排列
        for (int i = 0; i < 10 ; i++) {
            number.setCharAt(0, (char) (i+'0'));
            print1ToNCore(number,n,0);
        }
    }
    // 利用递归将字符串每一位都进行1到9的全排列
    private void print1ToNCore(StringBuilder number, int length, int index) {
        // 判断当前index已经指向最低位时,打印
        if (index == length-1){
            printNumber(number);
            return;
        }
        // 对每一位进行全排列
        for (int i = 0; i < 10 ; i++) {
            number.setCharAt(index+1,(char) (i+'0'));
            print1ToNCore(number,length,index+1);
        }
    }
    // 打印数字(剔除高位0)
    private void printNumber(StringBuilder number) {
        boolean isBegin0 = true;
        int nLength = number.length();
        // 剔除占位的0字符,打印出实际数字
        for (int i = 0; i < nLength; i++) {
            if (isBegin0 && number.charAt(i) != '0')
                isBegin0 = false;
            if (!isBegin0){
                System.out.print(number.charAt(i));
            }
        }
        System.out.print("\t");
    }
}

18. 删除链表中的节点

题目一:在O(1)时间内删除链表节点

给定单向链表的头节点和一个删除节点,定义一个方法在O(1)时间内删除该节点。

#链表节点
public class ListNode {
    int val;
    ListNode next = null;
    ListNode(int val) {
        this.val = val;
    }
}

题目详解

解法:

思路:如果节点i不是尾节点,先把i的下一个节点j的内容复制到i,然后把i.next指向j.next。如果i为尾节点,且为头节点,直接删除即可;若不为头节点,则从链表头节点开始,遍历的到删除节点的前节点,再完成删除操作。 时间复杂度:[(n-1)XO(1)+O(n)]/n, 还是O(1)

public class Main {
    public ListNode deleteNode(ListNode pListHead, ListNode pDelete) {
        // 排除特殊情况
        if (pListHead == null || pDelete == null)
            return null;
        // 删除节点不是尾节点
        if (pDelete.next != null) {
            ListNode pNext = pDelete.next;
            pDelete.val = pNext.val;
            pDelete.next = pNext.next;
        }
        // 只有一个节点删除头节点
        else if (pListHead == pDelete) {
            pListHead = null;
        }
        // 有多个节点删除尾节点
        else {
            ListNode pNode = pListHead;
            while (pNode.next != pDelete)
                pNode = pNode.next;
            pNode.next = null;
        }
        return pListHead;
    }
}

题目二:删除链表中重复的节点

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

→ 牛客网在线评判

题目详解

解法:

思路:从头遍历链表,如果当前节点的值与下一个节点值相同,那么他们是重复节点,可以删除。为了保证删除后链表仍然相连,需要把当前节点的前一个节点和后面值比当前节点值大的节点相连,确保前一个节点和下一个没有重复的节点连在一起。同时也要考虑全部节点值相同的情况。

public class Main {
    public ListNode deleteDuplication(ListNode pHead) {
        // 排除为空的情况
        if (pHead == null || pHead.next == null)
            return pHead;
        // 设置前一个节点
        ListNode pPreNode = null;
        // 从头节点开始查找
        ListNode pNode = pHead;
        while (pNode != null) {
            // 获取当前节点的下一个节点
            ListNode pNext = pNode.next;
            // 如果下一个节点不为空,且值和当前节点相等
            if (pNext!=null && pNext.val == pNode.val ) {
                // 获取删除值大小
                int value = pNode.val;
                // 设置检查节点
                ListNode pDelete = pNode;
                // 如果待删除节点不为空,且值与删除值相等
                while (pDelete != null && pDelete.val == value) {
                    // 获取删除节点下一个节点
                    pNext = pDelete.next;
                    // 将下一个节点设置为检查节点
                    pDelete = pNext;
                }
                // 判断是否删除了头节点
                if (pPreNode == null)
                    pHead = pNext;
                else
                    pPreNode.next = pNext;
                pNode = pNext;
            // 如果值不相等,直接查找下一个
            } else {
                pPreNode = pNode;
                pNode = pNext;
            }
        }
        return pHead;
    }
}

19. 正则表达式匹配

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:每次从字符串中拿出一个字符进行匹配。当期模式中的第二个字符不是''时,如果字符串中的第一个字符和模式中第一个字符相匹配,那么字符串和模式都向后移动一个字符,继续匹配;如果匹配不相等,则返回false。如果第二个字符是'',一种选择是在模式上向后移动两个字符,相当于忽视了'*'和它之前的字符;如果模式中的第一个字符和字符串中的第一个字符相匹配,则在字符串上向后移动一个字符,模式上有两种选择:一是向后移动两个字符;二是保持不变。

public class Main {
    public boolean match(char[] str, char[] pattern) {
        // 当数组为空直接返回false
        if (str == null ||pattern == null)
            return false;
        return matchCore(str,pattern,0,0);
    }
    // 正则表达式匹配核心代码
    private boolean matchCore(char[] str, char[] pattern,int i, int j) {
        // 如果已经匹配到最后一个字母
        if (i==str.length && j == pattern.length)
            return  true;
        // 如果未匹配完全
        if (i != str.length && j == pattern.length)
            return false;
        // 遇到'*'的情况
        if (j <pattern.length-1 && pattern[j + 1] == '*') {
            if (i != str.length && ( pattern[j] == str[i] || pattern[j] == '.') ) {
                return matchCore(str, pattern, i + 1, j + 2) ||
                        matchCore(str, pattern, i + 1, j) ||
                        matchCore(str, pattern, i, j + 2);
            } else {
                return matchCore(str, pattern, i, j + 2);
            }
        }
        // 遇到'.'的情况
        if (i != str.length && ( pattern[j] == str[i] || pattern[j] == '.'))
            return matchCore(str,pattern,i+1,j+1);
        return false;
    }
}

20. 表示数值的字符串

题目:

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。

→ 牛客网在线评判

题目详解

解法1:

思路:表示数值的字符串遵循模式A[.[B]][e|EC]或者.B[e|EC],其中A为数值的整数部分,B为数值的小数部分,C为指数部分。判断一个字符串是否符合要求时,首先尽可能多的扫描0到9的数位(可能在起始处有'+'或'-'),即数值整数A部分。如果遇到小数点'.',则开始扫描表示数值小数B部分。如果遇到'e'或'E',则开始扫描数值指数的C部分。

public class Main {
    public boolean isNumeric(char[] str) {
        // 排除字符串为空情况
        if (str == null)
            return false;
        // 设置检查索引
        int i = 0;
        boolean numeric = false;
        // 判断整数部分是否为数值
        if (scanInteger(str, i) > i) {
            numeric = true;
            i = scanInteger(str, i);
        }
        // 判断小数部分是否为数值
        if (i != str.length && str[i] == '.') {
            i++;
            if (scanUnsignedInteger(str,i) > i){
                numeric = true;
                i = scanUnsignedInteger(str,i);
            }
        }
        // 判断指数部分是否为数值
        if (i != str.length && (str[i] == 'e' || str[i] == 'E')) {
            i++;
            if (scanInteger(str, i) > i) {
                i = scanInteger(str, i);
            } else {
                numeric = false;
            }
        }
        return numeric && i==str.length;
    }
    // 检查有符号数值,并将检查索引后移
    private int scanInteger(char[] str, int i) {
        if (i != str.length && (str[i] == '+' || str[i] == '-'))
            i++;
        return scanUnsignedInteger(str,i);
    }
    // 检查无符号数值,并将检查索引后移
    private int scanUnsignedInteger(char[] str, int i) {
        while (i != str.length && str[i] >= '0' && str[i] <= '9' ) {
            i++;
        }
        return i;
    }
}
解法2:

思路:利用字符串matches()方法配合正则表达式,可以快速判断表达式是否符合要求格式。

public boolean isNumeric(char[] str) {
    return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?");
}

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

题目:

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组后半部分。

题目详解

解法1(不考虑相对顺序):

思路:使用两个索引,将第一个索引初始化指向数组第一个数字,它只向后移动;第二个索引初始化指向数组最后一个数字,它只向前移动。在两个索引相遇之前,第一个索引总是位于第二个索引前面。如果第一个指向的数字是偶数,且第二个指向的数字是奇数,则交换两个数字。

public class Main {
    public void reOrderArray(int [] array) {
        // 如果数组为空
        if (array == null || array.length  == 0) return;
        // 设置头和尾索引
        int iBegin = 0;
        int iEnd = array.length - 1;
        // 当头索引在尾索引之前时
        while (iBegin < iEnd) {
            // 查找偶数
            while (iBegin < iEnd && (array[iBegin] & 0x1) != 0)
                iBegin++;
            // 查找奇数
            while (iBegin < iEnd && (array[iEnd] & 0x1) == 0)
                iEnd--;
            // 交换数字
            if (iBegin < iEnd) {
                int tmp = array[iEnd];
                array[iEnd] = array[iBegin];
                array[iBegin] = tmp;
            }
        }
    }
}
解法2(考虑相对顺序):

思路:由于考虑相对顺序,所以只能从头遍历数组,判断每个数字是否为偶数,如果为偶数先将后面数字依次向前移动1格,并将当前偶数放到数组尾。

→ 牛客网在线评判
public class Main {
    public void reOrderArray(int [] array) {
        // 如果数组为空
        if (array == null || array.length  == 0) return;
        int length = array.length;
        boolean has = false;
        int first = -1;
        // 从头遍历
        for (int i = 0; i < length ; i++) {
            // 如果当前数字为偶数
            if ((array[i] & 0x1) == 0) {
                // 如果当前数字等于第一个偶数则调整完毕
                if (has && array[i] == first) {
                    break;
                }
                // 记录第一个偶数值
                if (!has) {
                    has = true;
                    first = array[i];
                }
                // 将当前数字放到数组尾,并调整数组
                int tmp = array[i];
                for (int j = i; j < length - 1 ; j++) {
                    array[j] = array[j+1];
                }
                array[length - 1] = tmp;
                // 从当前位置继续查找
                i = i-1;
            }
        }
    }
}

22. 链表中倒数第k个节点

题目:

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

→ 牛客网在线评判
// 链表结构
public class ListNode {
    int val;
    ListNode next = null;

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

题目详解

解法:

思路:定义两个索引,第一个从链表的头节点开始遍历向前走k-1步,第二个保持不动。从k步开始,第二个索引也从头节点开始遍历。两个索引保持k-1的距离,当第一个索引指向尾节点时,第二个索引正好指向倒数第k个节点。

public class Main {
    public ListNode FindKthToTail(ListNode head,int k) {
        // 如果为空链表
        if (head == null ||k <= 0)
            return null;
        // 设置两个索引
        ListNode pHead = head;
        ListNode pBehind = head;
        // 将第一个索引先走k-1步
        for (int i = 0; i < k-1 ; i++) {
            if (pHead.next != null) {
                pHead = pHead.next;
            } else {
                return null;
            }
        }
        // 两个索引一起遍历,直到尾节点
        while (pHead.next != null) {
            pHead = pHead.next;
            pBehind = pBehind.next;
        }
        return pBehind;
    }
}

23. 链表中环的入口节点

题目:

一个链表中包含环,请找出该链表的环的入口结点。

→ 牛客网在线评判
// 链表结构
public class ListNode {
    int val;
    ListNode next = null;

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

题目详解

解法:

思路:首先判断链表是否包含环并计算环的大小,设置两个索引,一个索引一次走1步,另一个走两步。如果走的快的追上走的慢的,则说明链表包含环;如果走的快的索引指向了链表末尾,则说明不包含环。记录快慢索引相遇的节点,从当前节点开始遍历,直到再次回到当前节点,并记录所走步数即为环的大小。接着找到环的入口,定义两个索引,初始化都指向链表头节点,如果环大小为n,先让一个索引向前移动n步,之后两个索引以相同速度移动。当两个索引指向同一个节点时,即为环的入口。

public class Main {
    public ListNode EntryNodeOfLoop(ListNode pHead) {
        ListNode meetNode = MeetingNode(pHead);
        if (meetNode == null) return null;
        // 计算环大小n
        int count = 1;
        ListNode pNode1 = meetNode;
        while (pNode1.next != meetNode) {
            pNode1 = pNode1.next;
            count++;
        }
        // 将第一个索引先移动n步
        pNode1 = pHead;
        for (int i = 0; i <count ; i++) {
            pNode1 = pNode1.next;
        }
        // 两个索引以相同速度移动,直到指向同一节点
        ListNode pNode2 = pHead;
        while (pNode1 != pNode2) {
            pNode1 = pNode1.next;
            pNode2 = pNode2.next;
        }
        return pNode1;
    }
    // 判断时候含有环,并返回环中一个节点
    private ListNode MeetingNode(ListNode pHead) {
        if (pHead == null) return null;
        ListNode slow = pHead.next;
        if (slow == null) return null;

        ListNode fast = slow.next;
        while (fast != null && slow != null) {
            if (fast == slow)
                return fast;
            slow = slow.next;
            fast = fast.next;
            if (fast != null && fast.next != null)
                fast = fast.next;
        }
        return null;
    }
}

24. 反转链表

题目:

输入一个链表,反转链表后,输出链表的所有元素。

→ 牛客网在线评判
// 链表结构
public class ListNode {
    int val;
    ListNode next = null;

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

题目详解

解法1(循环):

思路:调整结构时,首先保存当前节点node以及前一个节点pre和后一个节点next,将当前节点的next属性指向pre,同时将next作为新的当前节点继续调整。

public class Main {
    public ListNode ReverseList(ListNode head) {
        // 如果链表为空直接返回
        if (head == null) return null;
        // 创建节点保存反转后头节点
        ListNode reverseHead = null;
        // 创建节点保存当前,前一个,后一个节点
        ListNode node = head;
        ListNode pre = null;
        ListNode next = null;
        // 转置
        while (node != null) {
            // 获取下一个节点,如果为空则说明当前节点为尾节点
            next = node.next;
            if (next == null)
                reverseHead = node;
            // 将当前节点连接反转
            node.next = pre;
            // 保存下一个节点以及它的前节点
            pre = node;
            node = next;
        }
        return reverseHead;
    }
}
解法2(递归):

思路:思想同循环方法,用递归实现。

public class Main {
    public ListNode ReverseList(ListNode head) {
        // 如果链表为空直接返回
        if (head == null) return null;
       return ListNodeCore(null,head);
    }
    // 反转核心代码
    private ListNode ListNodeCore(ListNode pre, ListNode node) {
        // 获取下一个节点
        ListNode next = node.next;
        // 将当前节点连接反转
        node.next = pre;
        // 判断是否为尾节点
        if (next == null)
            return node;
        // 继续下一个节点反转
        return ListNodeCore(node,next);
    }
}

25. 合并两个排序链表

题目:

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

→ 牛客网在线评判
// 链表结构
public class ListNode {
    int val;
    ListNode next = null;

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

题目详解

解法(递归):

思路:从头节点开始比较,将节点值较小的值作为头节点,同时较小头节点指向下一个节点。继续合并两个链表中剩余节点,继续比较剩下链表头节点的值,将节点值较小的值和前面合并链表得到的链表尾节点连接起来。重复操作,直到链表合并完成。

public class Main {
    public ListNode Merge(ListNode list1,ListNode list2) {
        // 判断递归终止条件
        if (list1 == null)
            return list2;
        if (list2 == null)
            return list1;
        // 创建节点保存当前较小节点
        ListNode mergedHead = null;
        // 比较当前节点大小
        if (list1.val < list2.val) {
            mergedHead = list1;
            mergedHead.next = Merge(list1.next, list2);
        } else {
            mergedHead = list2;
            mergedHead.next = Merge(list1,list2.next);
        }
        // 返回合并后链表
        return mergedHead;
    }
}

26. 树的子结构

题目:

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

→ 牛客网在线评判
// 树节点
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

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

题目详解

解法(递归):

思路:首先遍历树A查找和树B根节点值一样的节点R;判断树A以R为根节点的子树是不是包含树B一样的结构;如果包含,返回成功;如果不包含,继续遍历A查找和树B根节点一样的节点,继续比较。

public class Main {
    public boolean HasSubtree(TreeNode root1,TreeNode root2) {
        boolean result = false;
        // 当root1和root2都不为空时,开始匹配
        if (root1 != null && root2 != null) {
            // 如果当前节点值相等,则判断是否为子树
            if (root1.val == root2.val)
                result = DoesTree1HasTress2(root1, root2);
            // 如果不是,继续遍历查找
            if (!result)
                result = HasSubtree(root1.left,root2);
            if (!result)
                result = HasSubtree(root1.right,root2);
        }
        return result;
    }
    // 判断是否为子树方法
    private boolean DoesTree1HasTress2(TreeNode root1, TreeNode root2) {
        // 子树判断完毕
        if (root2 == null)
            return true;
        // 查找完树1,没有下一个节点
        if (root1 == null)
            return false;
        // 如果当前节点值不相等
        if (root1.val != root2.val)
            return false;
        // 继续判断子节点
        return DoesTree1HasTress2(root1.left,root2.left) && DoesTree1HasTress2(root1.right,root2.right);
    }
}

27. 二叉树镜像

题目:

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

→ 牛客网在线评判
// 二叉树节点
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

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

题目详解

解法1(递归):

思路:先前序遍历这棵树的每个节点,如果遍历到的节点有子节点,就交换它的两个子节点。当交换完所有非叶子节点的左右子节点后,就得到树的镜像。

public class Main {
    public void Mirror(TreeNode root) {
        // 如果为空节点或叶子节点直接返回
        if (root == null)
            return;
        if (root.right == null && root.left == null)
            return;
        // 交换当前节点的左右子节点
        TreeNode tmp = root.right;
        root.right = root.left;
        root.left = tmp;
        // 继续交换当前子节点的子节点
        if (root.left != null)
            Mirror(root.left);
        if (root.right != null)
            Mirror(root.right);
    }
}
解法2(循环):

思路:思路同递归方法,在遍历树上使用栈辅助完成前序遍历。

import java.util.Stack;

public class Main {
    public void Mirror(TreeNode root) {
        // 如果为空节点或叶子节点直接返回
        if (root == null)
            return;
        // 利用栈保存遍历节点
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        TreeNode node;
        while (!stack.isEmpty()) {
            // 对遍历的非叶子节点进行交换
            node = stack.pop();
            exchange(node);
            // 前序遍历节点
            if (node.right != null)
                stack.push(node.right);
            if (node.left != null)
                stack.push(node.left);
        }
    }
    // 交换节点
    private void exchange(TreeNode node) {
        if (node.left != null || node.right != null) {
            TreeNode tmp = node.right;
            node.right = node.left;
            node.left = tmp;
        }
    }
}

28. 对称的二叉树

题目:

请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。

→ 牛客网在线评判
// 二叉树节点
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

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

题目详解

解法(递归):

思路:我们定义一种遍历方法,遍历完当前节点后,先遍历右子节点再遍历左子节点。如果当前树的这种遍历方法得到的序列和前序遍历得到的序列相同,则为对称二叉树。同时考虑到树中所有节点值相同的特殊情况,将null也作为空节点加入遍历。

public class Main {
    boolean isSymmetrical(TreeNode pRoot) {
        return isSymmetrical(pRoot,pRoot);
    }
    // 判断是否为对称二叉树方法
    private boolean isSymmetrical(TreeNode pRoot1, TreeNode pRoot2){
        // 将null也考虑为节点,避免所有节点值全相等特例
        if (pRoot1 == null && pRoot2 == null)
            return true;
        // 如果不同时为null则不为对称二叉树
        if (pRoot1 == null || pRoot2 ==null)
            return false;
        // 判断节点值是否相等
        if (pRoot1.val != pRoot2.val)
            return false;
        // 利用两种方法遍历,进行对比
        return isSymmetrical(pRoot1.left,pRoot2.right) && isSymmetrical(pRoot1.right,pRoot2.left);
    }
}

29. 顺时针打印矩阵

题目:

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下矩阵: 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。

→ 牛客网在线评判

题目详解

解法:

思路:首先考虑打印的终止条件,假设最左上角的坐标为(0,0),则终止条件为列数cols>startX2,行数rows>startY2,且startX=startY。接着考虑打印功能,分为四步:第一步,从左到右打印一行;第二步,从上到下打印一列;第三步,从右到左打印一行;第四步,从下到上打印一列。同时要考虑最后一圈退化的情况,可能只有一列,一行或者只有一个数字,并设置相应的判断条件。

import java.util.ArrayList;

public class Main {
    public ArrayList<Integer> printMatrix(int [][] matrix) {
        if (matrix == null) return null;
        // 获取矩阵长宽
        int rows = matrix.length;
        int cols = matrix[0].length;
        ArrayList<Integer> list = new ArrayList<>();
        int start = 0;
        // 设置终止条件
        while (rows > start * 2 && cols > start * 2) {
            int endX = cols - start - 1;
            int endY = rows - start - 1;
            // 从左到右打印一行
            for (int i = start; i <= endX ; i++) {
                list.add(matrix[start][i]);
            }
            // 从上到下打印一列
            if (start < endY) {
                for (int i = start + 1; i <= endY; i++) {
                    list.add(matrix[i][endX]);
                }
            }
            // 从右到左打印一行
            if (start < endX && start < endY) {
                for (int i = endX -1; i >= start ; i--) {
                    list.add(matrix[endY][i]);
                }
            }
            // 从下到上打印一列
            if (start < endX && start < endY - 1) {
                for (int i = endY -1; i > start ; i--) {
                    list.add(matrix[i][start]);
                }
            }
            // 遍历下一圈
            start++;
        }
        return list;
    }
}

30. 包含min函数的栈

题目:

定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的min函数。

→ 牛客网在线评判

题目详解

解法:

思路:利用两个栈,每次存入数据时,一个栈保存数据,另一个栈把每次的最小元素(之前的最小元素和新压入栈的元素两者的较小值)都保存起来放到另一个辅助栈里。每次弹出数据时,两个栈同时弹出栈定即可。而当前栈中最小值即为,最小值栈的栈顶。

import java.util.Stack;

public class Main {
    // 保存数据的栈
    private Stack<Integer> dataStack = new Stack<>();
    // 保存对应最小值的栈
    private Stack<Integer> minStack = new Stack<>();

    // push方法
    public void push(int node) {
        dataStack.push(node);
        // 将当前值和最小值栈顶对比
        if (minStack.isEmpty() || node < minStack.peek())
            minStack.push(node);
        else
            minStack.push(minStack.peek());
    }
    // pop方法
    public void pop() {
        dataStack.pop();
        minStack.pop();
    }
    // 获取栈顶方法
    public int top() {
        return dataStack.peek();
    }
    // 获取最小值方法
    public int min() {
        assert !dataStack.isEmpty() && !minStack.isEmpty();
        return minStack.peek();
    }
}

31. 栈的压入和弹出序列

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:利用辅助栈完成判断。依次将数据压入栈中,如果下一个弹出的数字刚好是栈顶数字,那么直接弹出;如果下一个弹出的数字不在栈顶,则把压栈序列中还没有入栈的数字压入辅助栈,指导把下一个需要弹出的数字压入栈顶为止;如果所有数字都压入栈后仍然没有找到下一个弹出的数字,则该序列不可能是一个弹出序列。

import java.util.Stack;

public class Main {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
        int length = pushA.length;
        boolean possible = false;
        if (pushA != null && popA != null && length > 0) {
            // 用栈进行数据操作
            Stack<Integer> stack = new Stack<>();
            // 设置查找索引
            int i,j = 0;
            // 遍历popA数组,进行判断
            for (i = 0; i < length; i++) {
                // 如果栈为空或当前栈顶数值不等于popA数组对应数值,将pushA数组值以此放入栈中
                for (; stack.isEmpty() || stack.peek() != popA[i] ; j++) {
                    // pushA所有数据都已经放入栈中
                    if (j == length)
                        break;
                    stack.push(pushA[j]);
                }
                // 如果栈顶元素和popA数组对应值不相等
                if (stack.peek() != popA[i])
                    break;
                // 当前数字比较完成,从栈中删除当前值
                stack.pop();
            }
            // 判断完全条件
            if (stack.isEmpty() && i == length)
                possible = true;
        }
        return possible;
    }
}

32. 从上到下打印二叉树

题目1:不分行从上到下打印二叉树

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

→ 牛客网在线评判

题目详解

解法:

思路:利用队列实现。从根节点开始,先打印当前节点,并将节点的左右子节点依次放入队尾。再从队头取出节点并打印,并且如果有子节点,依次放入队尾。直到打印完全。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;

public class Main {
    public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
        ArrayList<Integer> list = new ArrayList<>();
        // 如果为空节点
        if (root == null)
            return list;
        // 创建队列保存节点
        Queue<TreeNode> queue = new LinkedList<>();
        // 将头节点放入队列
        queue.offer(root);
        while (!queue.isEmpty()) {
            // 获取队头节点
            TreeNode node = queue.poll();
            // 打印
            list.add(node.val);
            // 将当前节点的子节点依次加入队尾
            if (node.left != null)
                queue.offer(node.left);
            if (node.right != null)
                queue.offer(node.right);
        }
        return list;
    }
}

题目2:分行从上到下打印二叉树

从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。

→ 牛客网在线评判

题目详解

解法:

思路:利用队列,并设置两个变量:一个表示当前层中还没有打印的节点数;另一个表示下一层节点的数目。之后遍历方法同题目1,只是打印时根据层次分组。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;

public class Main {
    public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> printList = new ArrayList<>();
        // 如果为空节点
        if (pRoot == null)
            return printList;
        // 创建队列保存节点,数组保存一行数据
        Queue<TreeNode> queue = new LinkedList<>();
        ArrayList<Integer> list = new ArrayList<>();
        // 将头节点放入队列
        queue.offer(pRoot);
        // 初始化下层节点数
        int nextLevel = 0;
        // 本层待打印数
        int toBePrinted = 1;
        while (!queue.isEmpty()) {
            // 获取队头节点并打印
            TreeNode node = queue.poll();
            list.add(node.val);
            // 将当前节点的子节点依次加入队尾
            if (node.left != null) {
                queue.offer(node.left);
                nextLevel++;
            }
            if (node.right != null) {
                queue.offer(node.right);
                nextLevel++;
            }
            // 减少待打印数
            toBePrinted--;
            // 如果当前层打印完全
            if (toBePrinted == 0) {
                printList.add(list);
                toBePrinted = nextLevel;
                nextLevel = 0;
                // 重新创建空数组
                list = new ArrayList<>();
            }
        }
        return printList;
    }
}

题目3:之字形打印二叉树

请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。

→ 牛客网在线评判

题目详解

解法:

思路:需要两个栈辅助。打印某一层节点时,把下一层的子节点保存到相应的栈里。如果当前打印的是奇数层,则先保存左子节点再保存右子节点到第一个栈里;如果当前打印的是偶数层,则先保存右子节点再保存左子节点到第二个栈里。

import java.util.ArrayList;
import java.util.Stack;

public class Main {
    public ArrayList<ArrayList<Integer> > Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer> > printList = new ArrayList<>();
        if (pRoot == null)
            return printList;
        // 创建两个栈保存当前层和下一层
        ArrayList<Stack> stacks = new ArrayList<>();
        stacks.add(new Stack<TreeNode>());
        stacks.add(new Stack<TreeNode>());
        // 创建数组保存当前层数据
        ArrayList<Integer> list = new ArrayList<>();
        // 设置当前层和下一层栈编号
        int current = 0;
        int next = 1;
        stacks.get(current).push(pRoot);
        // 进行打印
        while (!stacks.get(0).isEmpty() || !stacks.get(1).isEmpty()) {
            TreeNode node = (TreeNode) stacks.get(current).pop();
            list.add(node.val);
            // 判断是栈1还是栈2
            if (current == 0) {
                if (node.left != null)
                    stacks.get(next).push(node.left);
                if (node.right != null)
                    stacks.get(next).push(node.right);
            } else {
                if (node.right != null)
                    stacks.get(next).push(node.right);
                if (node.left != null)
                    stacks.get(next).push(node.left);
                }
             // 遍历完当前栈,对数据进行打印
            if (stacks.get(current).isEmpty()) {
                printList.add(list);
                list = new ArrayList<>();
                current = 1 - current;
                next = 1 - next;
            }
        }
        return printList;
    }
}

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

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:由后序遍历特点知,最后一个数字是树的根节点的值。数组中前面的数字可以分为两部分:第一部分是左子树节点的值,它们都比根节点的值小;第二部分是右子树节点的值,它们都比根节点的值大。之后再对左子树节点和右子树节点进行递归判断即可。

public class Main {
    public boolean VerifySquenceOfBST(int [] sequence) {
        int length = sequence.length;
        return VerifySquenceOfBSTCore(sequence,0,length);
    }
    // 判断核心方法
    private boolean VerifySquenceOfBSTCore(int[] sequence, int begin, int length) {
        if (sequence == null || length <= 0)
            return false;
        // 获取当前子树的根节点值
        int root = sequence[begin + length - 1];
        // 获取左子树开始位置,遍历得到左子树个数
        int i = begin;
        for (; i < begin + length - 1; i++) {
            if (sequence[i] > root)
                break;
        }
        // 获取右子树开始位置,遍历的到右子树个数
        int j = i;
        for (; j < begin+length - 1; j++) {
            if (sequence[j] < root)
                return false;
        }
        // 递归判断是否左子树的子树也符合条件
        boolean left = true;
        if (i > 0)
            left = VerifySquenceOfBSTCore(sequence,begin,i-begin);
        // 递归判断是否右子树的子树也符合条件
        boolean right = true;
        if (j < begin + length - 1)
            right = VerifySquenceOfBSTCore(sequence,begin + i,j - begin - i);
        // 返回判断结果
        return left && right;
    }
}

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

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:使用前序遍历方式访问到某一节点时,把节点添加到路劲上,并累加该节点的值。如果该节点为叶节点,并且路径中节点的值的和刚好等于输入的目标整数,则房钱路径符合要求,进行打印。如果当前节点不是叶节点,则继续访问它的子节点,并在访问之前删除当前节点并减去当前节点的值。

import java.util.ArrayList;

public class Main {
    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
        ArrayList<ArrayList<Integer>> targetList = new ArrayList<>();
        if (root == null)
            return targetList;
        // 创建数组保存路径值
        ArrayList<Integer> list = new ArrayList<>();
        int currentSum = 0;
        // 遍历树查找符合条件路径
        FindPath(root,target,currentSum,list,targetList);
        return targetList;
    }
    // 核心方法
    private void FindPath(TreeNode root, int target, int currentSum, 
                          ArrayList<Integer> list, ArrayList<ArrayList<Integer>> targetList) {
        // 将当前值添加到路径中
        currentSum += root.val;
        list.add(root.val);
        // 判断是够符合条件
        boolean isLeaf = (root.left == null && root.right == null);
        if (currentSum == target && isLeaf) {
            // 注意添加到总数组时,是将当前路径数组复制到其中
            targetList.add((ArrayList<Integer>) list.clone());
        }
        // 继续路径查找
        if (root.left != null)
            FindPath(root.left,target,currentSum,list,targetList);
        if (root.right != null)
            FindPath(root.right,target,currentSum,list,targetList);
        // 若不符合,将当前节点值从数组中删除
        list.remove(list.size() - 1);
    }
}

35. 复杂链表的复制

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:通过分治法实现:首先根据原始链表节点创建对应的节点,并将复制节点放到原始节点之后;接着原链表中random连接;最后拆分链表,得到复制链表。
时间复杂度:O(n)

public class Main {
    public RandomListNode Clone(RandomListNode pHead) {
        if (pHead == null)
            return null;
        // 复制通过3个方法实现
        CloneNodes(pHead);
        ConnectRandomNodes(pHead);
        return ReconnectNodes(pHead);
    }
    // 根据原始链表节点创建对应的节点,并将复制节点放到原始节点之后
    private void CloneNodes(RandomListNode pHead) {
        RandomListNode pNode = pHead;
        while (pNode != null) {
            // 复制节点
            RandomListNode pClone = new RandomListNode(pNode.label);
            pClone.next = pNode.next;
            // 连接节点
            pNode.next = pClone;
            pNode = pClone.next;
        }
    }
    // 复制random连接
    private void ConnectRandomNodes(RandomListNode pHead) {
        RandomListNode pNode = pHead;
        while (pNode != null) {
            RandomListNode pClone = pNode.next;
            if (pNode.random != null) {
                pClone.random = pNode.random.next;
            }
            pNode = pClone.next;
        }
    }
    // 拆分链表,得到复制链表
    private RandomListNode ReconnectNodes(RandomListNode pHead) {
        RandomListNode pCloneHead = pHead.next;
        RandomListNode pCloneNode = pCloneHead;
        RandomListNode pNode = pHead;
        while (pNode != null) {
            pNode.next = pCloneNode.next;
            pNode = pNode.next;
            if (pNode!= null)
                pCloneNode.next = pNode.next;
            pCloneNode = pCloneNode.next;
        }
        return pCloneHead;
    }
}

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

题目:

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

→ 牛客网在线评判

题目详解

解法:

思路:转换成排序双向链表时,原先指向左子节点的引用调整为链表中指向前一个节点的引用;原先指向右子节点的引用调整为链表中指向后一个节点的引用。同时利用中序遍历树中每个节点,当我们遍历到根节点时,它的左子树已经转换成一个排序的链表了,并且处在链表中的最后一个节点是当前值最大的节点;将最后一个节点和根节点连接起来,此时最后一个节点为根节点;接着再去遍历右子树,并把根节点和右子树中最小的节点链接起来。以此执行,直到结束。

public class Main {
    public TreeNode Convert(TreeNode pRootOfTree) {
        // 设置参数保存当前链表最后一个节点
        TreeNode pListLastNode = null;
        // 获取最后节点
        pListLastNode = ConvertCore(pRootOfTree,pListLastNode);
        // 通过向前查找的到链表头
        TreeNode pHead = pListLastNode;
        while (pHead != null && pHead.left != null)
            pHead = pHead.left;
        return pHead;
    }

    private TreeNode ConvertCore(TreeNode pNode, TreeNode pListLastNode) {
        if (pNode == null)
            return null;
        // 获取节点并向左查找更小值
        TreeNode pCurrent = pNode;
        if (pCurrent.left != null)
            pListLastNode = ConvertCore(pCurrent.left,pListLastNode);
        // 如果没有左子节点,将当前节点连接至链表尾
        pCurrent.left = pListLastNode;
        // 如果不是表头,将链表尾也指向当前节点
        if (pListLastNode != null)
            pListLastNode.right = pCurrent;
        // 设置新链表尾为当前节点
        pListLastNode = pCurrent;
        // 继续连接右子树
        if (pCurrent.right != null)
            pListLastNode = ConvertCore(pCurrent.right,pListLastNode);
        // 返回链表尾
        return pListLastNode;
    }
}

37. 序列化二叉树

题目:

请实现两个函数,分别用来序列化和反序列化二叉树

→ 牛客网在线评判

题目详解

解法:

思路:序列化:利用前序遍历序列化二叉树,将null作为空节点,并序列化为特殊字符,如"$,",同时节点之间用","进行分割。反序列化:先将序列化字符串分割成节点数组,并设置反序列化索引。利用前序遍历先访问根节点的特性,以此遍历节点数组进行重构。

public class Main {
    String Serialize(TreeNode root) {
        // 创建字符串保存序列化
        StringBuilder str = new StringBuilder();
        Serialize(root,str);
        return str.toString();
    }
    // 序列化核心方法
    private void Serialize(TreeNode node, StringBuilder str) {
        // 如果为空节点,序列化为"$,
        if (node == null) {
            str.append("$,");
            return;
        }
        // 序列化当前节点,并用","分割
        str.append(node.val);
        str.append(",");
        // 递归序列化所有节点
        Serialize(node.left,str);
        Serialize(node.right,str);
    }
    // 设置反序列化索引
    int index = -1;
    TreeNode Deserialize(String str) {
        // 获取每个节点数据,并保存到字符串数组中
        String[] strings = str.split(",");
        return Deserialize(strings);
    }
    // 反序列化核心方法
    private TreeNode Deserialize(String[] strings) {
        index++;
        TreeNode node = null;
        // 判断节点是否为"$",如果不是则反序列化节点
        if (!strings[index].equals("$")) {
            node = new TreeNode(Integer.valueOf(strings[index]));
            node.left = Deserialize(strings);
            node.right = Deserialize(strings);
        }
        return node;
    }
}

38. 字符串的排列

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:第一步求所有可能出现在第一个位置的字符,即把第一个字符和后面的所有字符交换。第二步,固定第一个字符,求后面所有字符的排列。对于求后面所有字符的排列,依旧利用这个思想递归执行。

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class Main {
    public ArrayList<String> Permutation(String str) {
        ArrayList<String> arrayList = new ArrayList<>();
        // 用HashSet保存结果单一值
        Set<String> set = new HashSet<>();
        if (str == null || str.length() == 0)
            return arrayList;
        // 字符串排列
        Permutation(str,set,0,"");
        // 转换为ArrayList格式
        arrayList.addAll(set);
        // 按字典序排序
        Collections.sort(arrayList);
        return arrayList;
    }
    // 排序核心方法
    private void Permutation(String str, Set<String> set,int index,String s) {
        // 判断当前是否排列完成
        if (index == str.length()) {
            set.add(s);
            return;
        } else {
            // 对后续字符串进行全排列组合
            for (int i = index; i < str.length(); i++) {
                // 交换当前字符到未排列字符串第一个位置
                char tmp = str.charAt(i);
                StringBuilder tmpStr = new StringBuilder(str);
                tmpStr.deleteCharAt(i);
                tmpStr.insert(index,tmp);
                str = tmpStr.toString();
                // 对后续字符串进行全排列
                Permutation(str,set,index+1,s+tmp);
            }
        }
    }
}

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

题目:

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

→ 牛客网在线评判

题目详解

解法:

思路:数组中一个数字出现次数超过数组长度的一半,即出现次数比其他所有数字出现次数的和还要多。在遍历数组的时候保存两个值:一个是数组中的一个数字;另一个是次数。当遍历下一个数字时,如果下一个数字和之前保存的数字相同,次数加1;如果下一个数字和之前保存的数字不同,次数减1.如果次数为0,则保存下一个数字,并设置次数为1。遍历完数组得到的数字即为所求。 时间复杂度:O(n)

public class Main {
    public int MoreThanHalfNum_Solution(int [] array) {
        int length = array.length;
        if (array == null || length <= 0)
            return 0;
        int result = array[0];
        int times = 1;
        // 如果存在超过一半的数字,则其他数字的个数和小于它的个数
        // 通过比较个数,最后剩下的数字即为超过一半的数字
        for (int i = 1; i < length ; i++) {
            if (times == 0) {
                result = array[i];
                times = 1;
            } else if (array[i] == result) {
                times++;
            } else {
                times--;
            }
        }
        if (!CheckMoreThanHalf(array,length,result))
            result = 0;
        return result;
    }
    // 判断数字是否超过一半方法
    private boolean CheckMoreThanHalf(int[] array, int length, int result) {
        int count = 0;
        for (int i = 0; i < length; i++) {
            if (array[i] == result)
                count++;
        }
        if (count*2 <= length)
            return false;
        return true;
    }
}

40. 最小的k个数

题目:

输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

→ 牛客网在线评判

题目详解

解法:

思路:创建一个最大堆。输入数字时,如果已有数字小于k个,直接把数字放入最大堆中。如果最大堆中数字个数已经为k个,则先和堆顶进行比较,如果小于堆顶,就将堆顶抛出,将当前值加入;如果大于,不添加。 时间复杂度:O(nlogk)

import java.util.ArrayList;
import java.util.Comparator;
import java.util.PriorityQueue;

public class Main {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> result = new ArrayList<>();
        int length = input.length;
        if (input == null || k <= 0 || length < k)
            return result;
        // 重写比较器,将最小堆变为最大堆
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        // 如果堆中数据小于k,直接添加
        // 如果数据大于k,则和堆顶比较,如果小于就替换
        for (int i = 0; i < length ; i++) {
            if (maxHeap.size() != k)
                maxHeap.offer(input[i]);
            else if (maxHeap.peek() > input[i]) {
                maxHeap.poll();
                maxHeap.offer(input[i]);
            }
        }
        for (Integer integer : maxHeap)
            result.add(integer);
        return result;
    }
}

41. 数据流中的中位数

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:创建一个最小堆和一个最大堆,添加数据时保持最大堆所有数据均小于最小堆里数据。每次添加数据时,如果数据的总数目是偶数,先将数据放入最大堆,并取最大堆堆顶放入最小堆;当总数为奇数时,先将数据放入最小堆,并取最小堆堆顶放入最大堆,直到所有数据添加完毕。取中位数时,若总数为偶数,则中值为堆顶的平均值;若总数为奇数时,中值为最小堆堆顶; 时间复杂度:O(logn)

import java.util.Comparator;
import java.util.PriorityQueue;

public class Main {
    // 记录数据总数,创建大小堆
    private int count = 0;
    private PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    private PriorityQueue<Integer> maxHeap = new PriorityQueue<>(15, new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    });
    public void Insert(Integer num) {
        if (count % 2 == 0) { 
            // 当总数为偶数时,先将数据放入最大堆,并取最大堆堆顶放入最小堆
            maxHeap.offer(num);
            int maxNumInMaxHeap = maxHeap.poll();
            minHeap.offer(maxNumInMaxHeap);
        } else {
            // 当总数为奇数时,先将数据放入最小堆,并取最小堆堆顶放入最大堆
            minHeap.offer(num);
            int minNumInMinHeap = minHeap.poll();
            maxHeap.offer(minNumInMinHeap);
        }
        count++;
    }

    public Double GetMedian() {
        if (count % 2 == 0)
            // 总数为偶数,则中值为堆顶的平均值
            return new Double(minHeap.peek()+maxHeap.peek()) / 2;
        else
            // 总数为奇数时,中值为最小堆堆顶
            return new Double(minHeap.peek());
    }
}

42. 连续子数组的最大值

题目:

输入一个整数数组,数组里有正数也有负数。数组中一个或连续的多个整数组成一个子数组。求所有子数组的和的最大值。

→ 牛客网在线评判

题目详解

解法:

思路:从头开始逐个累加。如果累加当前值后,和数小于0,则从当前数开始从新累加。同时比较当前和、最大和值,如果过当前和大于最大和,则进行替换。 时间复杂度:O(n)

public class Main {
    public int FindGreatestSumOfSubArray(int[] array) {
        int length = array.length;
        if (array == null || length <= 0)
            return 0;
        // 设置当前和以及最大和
        int sum = 0;
        int greatSum = 0x80000000;;
        for (int i = 0; i < length ; i++) {
            if (sum > 0)
                sum += array[i];
            else
                sum = array[i];

            if (sum > greatSum)
                greatSum = sum;
        }
        return greatSum;
    }
}

43. 1到n整数1出现大的次数

题目:

输入一个整数n,求1到n这n个整数的十进制表示中1出现的次数。例如,输入12,输出为1/10/11/12,1一共出现了5次。

→ 牛客网在线评判

题目详解

解法:

思路:1)如果第i位(自右至左,从1开始标号)上的数字为0,则第i位可能出现1的次数由更高位决定(若没有高位,视高位为0),等于更高位数字X当前位数的权重10^i-1^。2)如果第i位上的数字为1,则第i位上可能出现1的次数不仅受更高位影响,还受低位影响(若没有低位,视低位为0),等于更高位数字X当前位数的权重10^i-1^+(低位数字+1)。3)如果第i位上的数字大于1,则第i位上可能出现1的次数仅由更高位决定(若没有高位,视高位为0),等于(更高位数字+1)X当前位数的权重10^i-1^。 时间复杂度:O(logn)

public class Main {
    public int NumberOf1Between1AndN_Solution(int n) {
       if (n <= 0)
           return 0;
       int high,low, curr, tmp, i = 1;
       high = n;
       int total = 0;
        while (high != 0) {
            // 求高位个数
            high = n / (int)Math.pow(10,i);
            tmp = n % (int)Math.pow(10,i);
            curr = tmp / (int)Math.pow(10,i - 1);
            // 求低位个数
            low = tmp % (int)Math.pow(10,i - 1);
            // 求总数
            if (curr == 1)
                total += high * (int)Math.pow(10, i -1) + low + 1;
            else if (curr < 1)
                total += high * (int)Math.pow(10, i - 1);
            else
                total += (high + 1) * (int)Math.pow(10, i - 1);
            i++;
        }
        return total;
    }
}

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

题目:

数字以0123456789101112131415...的格式序列化到一个字符序列中。在这个序列中,第五位(从0开始计数)是5,第13位是1,第19位是4,等等。写一个方法,求任意第n位对应的数字。

题目详解

解法:

思路:先从1位数开始,获取1位数的总数,比较索引。从索引总减去1位数的索引个数,在和两位数比较...直到定位到几位数,再根据索引值定位到具体数字,从而定位到具体数字的第几个数,最后输出。

public class Main {
    public int digitAtIndex(int index) {
        if (index < 0)
            return -1;
        // 从个位开始判断
        int digits = 1;
        while (true) {
            // 获取对应位数总个数
            int numbers = countOfIntegers(digits);
            // 判断是否在范围内
            if (index < numbers * digits)
                return digitAtIndex(index,digits);
            index -= digits * numbers;
            digits++;
        }
    }
    // 得到digits位数字总共多少个
    private int countOfIntegers(int digits) {
        if (digits == 1)
            return 10;
        return 9 * (int)Math.pow(10,digits - 1);
    }
    // 找出索引对应数字
    private int digitAtIndex(int index, int digits) {
        // 定位对应的digits位数字
        int number = beginNumber(digits) + index/digits;
        // 获取所求数字在其中的索引
        int indexFromRight = digits - index % digits;
        for (int i = 1; i < indexFromRight; i++)
            number /= 10;
        // 得到对应数字
        return number % 10;
    }
    // 获得digits位数字的第一个数字
    private int beginNumber(int digits) {
        if (digits == 1)
            return 0;
        return (int) Math.pow(10,digits - 1);
    }
}

45. 把数组排成最小的数

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:将int数组转换成字符串数组,之后根据数字m和n拼接起来得到mn和nm,它们位数相同,再按照字符串大小的比较规则比较就行。利用这个比较规则对字符串排序,最后拼接所有字符串输出即为最小数字。

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class Main {
    public String PrintMinNumber(int [] numbers) {
        // 将当前int数组转换为String数组
        ArrayList<String> list = new ArrayList<>();
        for (int i: numbers) {
            list.add(String.valueOf(i));
        }
        // 重写Comparator设置新的比较方法
        // 重新排序
        Collections.sort(list, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                String str1 = o1 + o2;
                String str2 = o2 + o1;
                return str1.compareTo(str2);
            }
        });
        // 将结果转换成String
        String result = "";
        for (String str: list) {
            result += str;
        }
        return result;
    }
}

46. 把数字翻译成字符串

题目:

给定一个数字,按照如下规则把它翻译为字符串:0翻译成"a",1翻译成"b",...,11翻译成"l",...,25翻译成"z"。一个数字可能有多个翻译。编写一个函数,用来计算一个数字有多少种不同的翻译方法。

题目详解

解法:

思路:定义函数f(i)表示第i位开始的不同翻译的数目,可以得到公式:

f(i) = f(i + 1) + g(i,i+1)*f(i+2)

当第i位和第i+1位两位数字拼接起来的数字在10到25范围之内时,函数 g(i,i+1)的值为1;否则为0.

public class Main {
   public int GetTranslationCount(int number) {
       if (number < 0)
           return 0;
       // 将数字转换为String类型
       String strNumber = String.valueOf(number);
       return GetTranslationCount(strNumber);
   }

    private int GetTranslationCount(String strNumber) {
        int length = strNumber.length();
        // 设置数组保存不同长度对应翻译数
        int[] counts = new int[length];
        // 设置当前翻译数
        int count = 0;
        for (int i = length - 1; i >= 0 ; i--) {
            count = 0;
            // 获取上一次翻译个数
            if (i < length - 1 )
                count = counts[i+1];
            else
                count = 1;
            // 通过计算获得本次翻译数
            if (i < length - 1) {
                int digit1 = strNumber.charAt(i) - '0';
                int digit2 = strNumber.charAt(i + 1) - '0';
                int converted = digit1 * 10 + digit2;
                if (converted >= 10 && converted <= 25) {
                    if (i < length - 2 )
                        count += counts[i + 2];
                    else
                        count += 1;
                }
            }
            // 将当前翻译数保存到数组中
            counts[i] = count;
        }
        // 返回完整数据对应翻译数
        count = counts[0];
        return count;
   }
}

47. 礼物的最大价值

题目:

在一个m x n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值。从棋盘左上角开始拿格子里的礼物,并每次向左或者向下移动一格,直到到达棋盘的右下角。给定一个棋盘及其上面的礼物,计算你最多能拿到多少价值的礼物。

题目详解

解法:

思路:定义函数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 class Main {
   public int getMaxValue(int[][] values) {
       int rows = values.length;
       int cols = values[0].length;
       if (values == null || rows <= 0 || cols <= 0)
           return 0;
       // 用数组保存最大值,前j个数字为第i行前面j个格子礼物的最大值
       // 之后数字保存前面i - 1行n - j个格子礼物的最大值
       int[] maxValues = new int[cols];
       for (int i = 0; i < rows ; i++) {
           for (int j = 0; j < cols ; j++) {
               int left = 0;
               int up = 0;
               // 获取当前格子左边和上边格子礼物的最大值
               if (i > 0)
                   up = maxValues[j];
               if (j > 0)
                   left = maxValues[j - 1];
               // 计算当前格子礼物最大值
               maxValues[j] = Math.max(left,up) + values[i][j];
           }
       }
       int maxValue = maxValues[cols - 1];
       return maxValue;
   }
}

48. 最长不含重复字符的子字符串

题目:

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

题目详解

解法:

思路:利用动态规划求解。定义函数f(i)表示以第i个字符为结尾的不包含重复字符的子字符串的最大长度。从左到右遍历字符串,当计算f(i)时,已经知道f(i-1)的值,如果第i个字符之前没有出现过,f(i)=f(i-1)+1;如果第i个字符之前已经出现过,先计算第i个字符和它上次出现位置的距离,记录为d,如果d小于或者等于f(i-1).得到f(i)=d;如果d大于f(i-1),有f(i)=f(i-1)+1。

public class Main {
    public int longestSubstring(String str) {
        int curLength = 0;
        int maxLength = 0;
        // 穿件数组,保存字符上一次出现的位置
        int[] position = new int[26];
        // 初始化为 -1
        for (int i = 0; i < 26; i++) {
            position[i] = -1;
        }
        int length = str.length();
        // 遍历字符串
        for (int i = 0; i < length; i++) {
            // 获取当前字符上次出现位置
            int prevIndex = position[str.charAt(i) - 'a'];
            // 如果没出现过或者出现位置大于curLength
            if (prevIndex < 0 || i - prevIndex > curLength)
                curLength++;
            else {
            // 如果在curLength长度内出现    
                if (curLength > maxLength)
                    maxLength = curLength;
                curLength = i - prevIndex;
            }
            // 重新设置字符出现位置
            position[str.charAt(i) - 'a'] = i;
        }
        if (curLength > maxLength)
            maxLength = curLength;
        return maxLength;
    }
}

49. 丑数

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:创建一个数组,里面的数字是排好序的丑数,每个丑数都是前面的丑数乘以2/3或者5得到的。

public class Main {
    public int GetUglyNumber_Solution(int index) {
        if (index <= 0)
            return 0;
        // 创建数组按顺序记录丑数
        int[] uglyNumbers = new int[index];
        uglyNumbers[0] = 1;
        int nextUglyIndex = 1;
        // 设置乘2/3/5丑数索引
        int multiply2 = 0;
        int multiply3 = 0;
        int multiply5 = 0;
        while (nextUglyIndex < index) {
            // 将当前相乘后最小的丑数添加到数组中
            int min = min(uglyNumbers[multiply2] * 2, uglyNumbers[multiply3] * 3, uglyNumbers[multiply5] * 5);
            uglyNumbers[nextUglyIndex] = min;
            // 同时过滤相乘后小于当前最大丑数的值
            while (uglyNumbers[multiply2] * 2 <= uglyNumbers[nextUglyIndex])
                multiply2++;
            while (uglyNumbers[multiply3] * 3 <= uglyNumbers[nextUglyIndex])
                multiply3++;
            while (uglyNumbers[multiply5] * 5 <= uglyNumbers[nextUglyIndex])
                multiply5++;
            nextUglyIndex++;
        }
        // 获取所求丑数
        int ugly = uglyNumbers[nextUglyIndex - 1];
        return ugly;
    }
    // 获取三个数最小值
    private int min(int number1, int number2, int number3) {
        int min = (number1 < number2) ? number1:number2;
        return  (min < number3) ? min : number3;
    }
}

50. 第一个只出现一次的字符

题目一:字符串中第一个只出现一次的字符

在一个字符串(1<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置。

→ 牛客网在线评判

题目详解

解法:

思路:利用数组创建hash表,key值为索引值。遍历字符串,每遍历到一个就将hash表对应位置加1。扫描完成后,在遍历字符串找出第一个在hash表中字符个数为1的字符位置并输出。 时间复杂度:O(n)

public class Main {
    public int FirstNotRepeatingChar(String str) {
        int length = str.length();
        if (str == null || length == 0)
            return -1;
        // 创建数组保存所有字符个数
        int[] hashTable = new int[256];
        // 遍历字符串并将个数添加到数组中
        for (int i = 0; i < length; i++) {
            hashTable[str.charAt(i)]++;
        }
        // 查找第一个出现一次字符的位置
        for (int i = 0; i < length; i++) {
            if (hashTable[str.charAt(i)] == 1)
                return i;
        }
        return -1;
    }
}

题目二:字符流中第一个只出现一次的字符

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

→ 牛客网在线评判

题目详解

解法:

思路:将读出的字符记录到hash表中,哈希表初始值为-1,如果是第一次出现,则修改值为字符串位置,如果是多次出现则修改为-2。之后遍历hash表,找出第一个出现字符。 时间复杂度:O(n)

public class Main {
    // 利用数组创建hash表,key值为索引
    private int[] occurence = new int[256];
    private int index;
    // 初始化参数值
    {
        for (int i = 0; i < 256; i++) {
            occurence[i] = -1;
        }
        index = 0;
    }
    // 输入字符流
    public void Insert(char ch) {
        if (occurence[ch] == -1)
            occurence[ch] = index;
        else if (occurence[ch] >= 0)
            occurence[ch] = -2;
        index++;
    }
    // 返回第一个不重复字符
    public char FirstAppearingOnce() {
        char ch = '#';
        int minIndex = Integer.MAX_VALUE;
        for (int i = 0; i < 256 ; i++) {
            if (occurence[i] >= 0 && occurence[i] < minIndex){
                ch = (char) i;
                minIndex = occurence[i];
            }
        }
        return ch;
    }
}

51. 数组中的逆序对

题目:

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007

→ 牛客网在线评判

题目详解

解法:

思路:先把数组分割成子数组,统计出子数组内部的逆序对数目,然后再统计出两个相邻子数组之间的逆序对的数目。在统计逆序对的过程中,还需要对数组进行排序,即为归并排序。 时间复杂度:O(nlogn) 空间复杂度:O(n)

public class Main {
    public int InversePairs(int [] array) {
        int length = array.length;
        if (array == null || length <= 0)
            return 0;
        // 创建辅助数组
        int[] copy = new int[length];
        for (int i = 0; i < length ; i++) {
            copy[i] = array[i];
        }
        // 归并排序查找
        return InversePairs(array, copy, 0, length - 1);
    }

    private int InversePairs(int[] array, int[] copy, int start, int end) {
        if (start == end){
            copy[start] = array[start];
            return 0;
        }
        // 获取中点值
        int length = (end - start) / 2;
        // 分别计算两边
        int left = InversePairs(copy, array, start , start + length)%1000000007;
        int right = InversePairs(copy, array, start + length + 1 , end)%1000000007;
        // 初始化i为前半段最后一个数字下标
        int i = start + length;
        // 初始化j为后半段最后一个数字下标
        int j = end;
        // 设置辅助数组复制初识位置
        int indexCopy = end;
        int count = 0;
        while (i >= start && j >= start + length + 1) {
            // 如果前半段数字大于后半段
            if (array[i] > array[j]) {
                // 计算逆序个数
                copy[indexCopy--] = array[i--];
                count += j - start - length;
                if (count >= 1000000007)
                    count %= 1000000007;
            } else {
                copy[indexCopy--] = array[j--];
            }
        }
        // 将剩余数字进行复制
        for (;i >= start; --i) {
            copy[indexCopy--] = array[i];
        }
        for (; j >= start + length + 1; --j) {
            copy[indexCopy--] = array[j];
        }
        // 返回所有逆序数
        return (left + right + count)%1000000007;
    }
}

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

题目:

输入两个链表,找出它们的第一个公共结点。

→ 牛客网在线评判

题目详解

解法:

思路:首先遍历两个链表得到它们长度,能确定那个链表较长,并且知道比短链表多几个节点。第二次遍历时,让较长链表先走上若干步,接着两个链表同时遍历,找到的第一个相同的节点即为第一个公共节点。 时间复杂度:O(m+n)

public class Main {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        if (pHead1 == null || pHead2 == null)
            return null;
        // 获取链表长度
        int length1 = getListLength(pHead1);
        int length2 = getListLength(pHead2);
        // 获得较长和较短链表头,并计算长度差值
        int lengthDif = (length1 > length2) ? (length1 - length2) : (length2 - length1);
        ListNode pListHeadLong = (length1 > length2) ? pHead1 : pHead2;
        ListNode pListHeadShort =(length1 > length2) ? pHead2 : pHead1;
        // 先移动较长链表差值长度距离
        for (int i = 0; i < lengthDif ; i++) {
            pListHeadLong = pListHeadLong.next;
        }
        // 同时向后查找,指导相同节点为止
        while (pListHeadLong != null && pListHeadShort != null && pListHeadLong != pListHeadShort) {
            pListHeadLong = pListHeadLong.next;
            pListHeadShort = pListHeadShort.next;
        }
        // 返回相同节点
        return pListHeadLong;
    }
    // 计算链表长度
    private int getListLength(ListNode pHead) {
        int length = 0;
        ListNode pNode = pHead;
        while (pNode.next != null) {
            length++;
            pNode = pNode.next;
        }
        return length;
    }
}

53. 在排序数组中查找数字

题目一:数字在排序数组中出现的次数

统计一个数字在排序数组中出现的次数。

→ 牛客网在线评判

题目详解

解法:

思路:利用二分查找算法查找第一个k和最后一个k。查找第一个k时,将数组中间的数字和k作比较,如果中间数字比k大,那么k只会出现在前半段;如果中间数字比k小,那么k只会出现在后半段;如果中间数字和k相等,如果中间数字前一个数字不是k,那么中间数字即为第一个k,如果前面也是k,则继续查找前半段。查找最后一个k时,如果中间数字比k大,则查找前半段;如果中间数字比k小,则查找后半段;如果中间数字和k相等,如果下一个数字不是k,则即为最后一个k,否则继续查找后半段。 时间复杂度:O(logn)

public class Main {
    public int GetNumberOfK(int [] array , int k) {
        int number = 0;
        int length = array.length;
        if (array != null && length > 0) {
            // 获取第一个和最后一个k的位置
            int first = getFirstK(array, length, k, 0, length - 1);
            int last = getLastK(array, length, k, 0, length - 1);
            // 计算个数
            if (first > -1 && last > -1)
                number = last - first + 1;
        }
        return number;
    }
    // 计算第一个k的位置
    private int getFirstK(int[] array, int length, int k, int start, int end) {
        // 设置递归结束判断
        if (start > end)
            return -1;
        // 利用二分查找思想找到第一个k的索引
        int midIndex = (start + end) >> 1;
        int midData = array[midIndex];
        if (midData == k) {
            // 如果当前索引的前一位值不为k或为数组第一个数字,对应索引即为所求
            if ((midIndex > 0 && array[midIndex - 1] != k) || midIndex == 0)
                return midIndex;
            else
                end = midIndex - 1;
        } else if (midData > k)
            end = midIndex - 1;
        else
            start = midIndex + 1;
        return  getFirstK(array, length, k, start, end);
    }
    // 计算最后一个k的位置
    private int getLastK(int[] array, int length, int k, int start, int end) {
        if (start > end)
            return  -1;
        // 利用二分查找思想找到最后一个k的索引
        int midIndex = (start + end) >> 1;
        int midData = array[midIndex];
        if (midData == k) {
            // 如果当前索引的后一位不为k或者为数组最后一个数字,对应索引即为所求
            if ((midIndex < length - 1 && array[midIndex + 1] != k) || midIndex == length - 1)
                return midIndex;
            else
                start = midIndex + 1;
        } else  if (midData < k)
            start = midIndex + 1;
        else
            end = midIndex - 1;
        return getLastK(array, length, k, start, end);
    }
}

题目二:0到n-1中缺失的数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0到n-1之内。在范围0到n-1内的n给数字有且只有一个数字不在数组中,找出这个数字。

题目详解

解法:

思路:基于二分查找算法查找:如果中间元素的值和下标相等,则继续查找后半段;如果中间元素值和下标不相等,并且前一个元素值和下标相等,则当前下标即为所求,否则继续查找前半段。 时间复杂度:O(logn)

public class Main {
    public int getMissingNumber(int[] numbers) {
        int length = numbers.length;
        if (numbers == null || length <= 0)
            return -1;
        int left = 0;
        int right = length - 1;
        // 利用二分查找思想,找到第一个数值和索引不相等的位置即为所求
        while (left <= right) {
            int mid = (right + left) >> 1;
            if (numbers[mid] != mid) {
                // 如果当期位置前一位数字和索引相等
                if (mid == 0 || numbers[mid -1] == mid - 1)
                    return mid;
                right  = mid - 1;
            } else
                left = mid + 1;
        }
        if (left == length)
            return length;
        return -1;
    }
}

题目三:数组中数值和下标相等的元素

假设一个单调递减的数组里的每个元素都是整数并且是唯一的。请编程实现一个函数,找出数组中任意一个数值等于其下标的元素。

题目详解

解法:

思路:基于二分查找算法查找:如果第i个数字的值大于i,则右边的数字都大于对应下标,只需要查找左边;如果第i个数字的值小于i,则左边所有数字的值都小于对应下标,只需要查找右边。 时间复杂度:O(logn)

public class Main {
    public int getNumberSameAsIndex(int[] numbers) {
        int length = numbers.length;
        if (numbers == null || length <= 0)
            return -1;
        // 利用二分查找思想,找到数值和索引相等的元素
        int left = 0;
        int right = length - 1;
        while (left <= right) {
            int mid = (left + right) >> 1;
            if (numbers[mid] == mid)
                return mid;
            // 如果当前中点元素大于索引值,从左半边继续查找
            if (numbers[mid] > mid)
                right = mid - 1;
            // 如果当前中点元素小于索引值,从右半边继续查找
            else
                left = mid + 1;
        }
        return -1;
    }
}

54. 二叉搜索树的第k大节点

题目:

给定一颗二叉搜索树,请找出其中的第k大的结点

→ 牛客网在线评判

题目详解

解法:

思路:利用中序遍历二叉搜索树,则遍历的第k个节点即为其中第k大节点。

import java.util.Stack;

public class Main {
    TreeNode KthNode(TreeNode pRoot, int k) {
        if (pRoot == null || k <= 0)
            return null;
        int count = 0;
        // 利用中序遍历查找到第k个节点
        TreeNode target = null;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode pNode = pRoot;
        while (pNode != null || !stack.isEmpty()) {
            // 遍历左节点
            while (pNode != null) {
                stack.push(pNode);
                pNode = pNode.left;
            }
            // 遍历当前节点
            pNode = stack.pop();
            count++;
            if (count == k)
                target = pNode;
            // 遍历右节点
            pNode = pNode.right;
        }
        return target;
    }
}

55. 二叉树的深度

题目一:二叉树的深度

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

→ 牛客网在线评判

题目详解

解法:

思路:如果树只有一个节点,深度为1;如果根节点只有左子树而没有右子树,则深度为左子树深度加1;同理,若果只有右子树,则深度为右子树深度加1;如果左右子树都有,深度即为左右子树深度较大值加1。

public class Main {
    public int TreeDepth(TreeNode root) {
        if (root == null)
            return 0;
        // 获取左右子树的深度
        int left = TreeDepth(root.left);
        int right = TreeDepth(root.right);
        // 比较左右子树深度大小,取较大值加1
        return (left > right) ? (left + 1) : (right + 1);
    }
}

题目二:平衡二叉树

输入一棵二叉树,判断该二叉树是否是平衡二叉树。

→ 牛客网在线评判

题目详解

解法:

思路:利用后序遍历二叉树每个节点,那么遍历到一个节点之前就已经遍历了它的左右子树。在遍历每个节点时记录它的深度,就可以判断每个节点是否是平衡的。

public class Main {
    public boolean IsBalanced_Solution(TreeNode root) {
        return IsBalanced_Solution(root, new Depth());
    }
    // 利用内部类进行传值
    private class Depth {
        int dep;
    }
    // 判断核心方法
    private boolean IsBalanced_Solution(TreeNode root, Depth depth) {
        // 设置递归终止条件
        if (root == null) {
            depth.dep = 0;
            return true;
        }
        // 穿件类保存左右子树深度
        Depth left = new Depth();
        Depth right = new Depth();
        // 判断左右子树是否为平衡二叉树
        if (IsBalanced_Solution(root.left, left) && IsBalanced_Solution(root.right, right)) {
            int diff = left.dep - right.dep;
            // 判断当前子树是否为平衡二叉树
            if (diff <= 1 && diff >= -1) {
                depth.dep = (left.dep > right.dep ? left.dep : right.dep) + 1;
                return true;
            }
        }
        return false;
    }
}

56. 数组中数字出现的次数

题目一:数组中只出现一次的两个数字

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

→ 牛客网在线评判

题目详解

解法:

思路:从头到尾异或数组中的每个数字,最终得到结果就是两个只出现一次的异或结果。由于这两个数字肯定不一样,异或结果不为0,即结果二进制表示至少有一位为1.在结果数字中找到第一个为1的位置,记为第n位。以第n位是不是1为标准把原数组分成两个子数组,分别对两个子数组进行异或操作,结果即为两个出现一次的数字。 时间复杂度:O(n) 空间复杂度:O(1)

public class Main {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        int length = array.length;
        if (array == null || length < 2)
            return;
        // 异或数组所有数字,找出不相同两个数字的异或
        int resultExclusiveOR = 0;
        for (int i = 0; i < length; i++)
            resultExclusiveOR ^= array[i];
        // 找到异或结果第一个为1位的索引
        int indexOf1 = findFirstBitIs1(resultExclusiveOR);
        // 将数组拆成两部分进行异或,得到两个只出现一次数字
        num1[0] = 0;
        num2[0] = 0;
        for (int i = 0; i < length; i++) {
            if (isBit1(array[i], indexOf1))
                num1[0] ^= array[i];
            else
                num2[0] ^= array[i];
        }
    }
    // 找到异或结果第一个为1位的索引
    private int findFirstBitIs1(int num) {
        int index = 0;
        while (((num & 1) == 0) && (index < 8 * 32)) {
            num = num >> 1;
            index++;
        }
        return index;
    }
    // 判断当前数字第index位是否为1
    private boolean isBit1(int num, int index) {
        num = num >> index;
        return (num & 1) == 1 ;
    }
}

题目二:数组中唯一只出现一次的数字

在一个数组中除了一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

题目详解

解法:

思路:将数组中所有数字的二进制表示的每一位都加起来。如果某位的和能被3整除,那么只出现一次的数字二进制表示中对应的哪一位为0;否则为1. 时间复杂度:O(n) 空间复杂度:O(1)

public class Main {
    public int findNumberAppearingOnce(int[] numbers) throws Throwable {
        int length = numbers.length;
        if (numbers == null || length <= 0)
            throw new Throwable("Invalid input");
        // 统计每位出现次数
        int[] bitSum = new int[32];
        for (int i = 0; i < length; i++) {
            int bitMask = 1;
            int num = numbers[i];
            // 对每个数字进行统计
            for (int j = 31; j >= 0 ; j--) {
                int bit = (num % 2) & bitMask;
                if (bit != 0)
                    bitSum[j]++;
                num /= 2;
            }
        }
        // 找那个出现1次的数字
        int result = 0;
        for (int i = 0; i < 32; i++) {
            result = result << 1;
            result += bitSum[i] % 3;
        }
        return result;
    }
}

57. 和为S的数字

题目一:和为S的两个数字

输入一个递增排序的数组和一个数字S,在数组中查找两个数,是的他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

→ 牛客网在线评判

题目详解

解法:

思路:定义两个索引,第一个指向数组第一个数字,另一个指向最后一个数字;计算两个数字的和,如果大于S,则将后一个索引前移1位,继续计算和;如果和小于S,则将前一个索引后移1位;直到找到满足条件结果为止。 时间复杂度:O(n)

import java.util.ArrayList;

public class Main {
    public ArrayList<Integer> FindNumbersWithSum(int [] array, int sum) {
        int length = array.length;
        ArrayList<Integer> list = new ArrayList<>();
        if (array == null || length <= 0)
            return list;
        // 设置两个索引指向数组头尾
        int ahead = length - 1;
        int behind = 0;
        while (ahead > behind) {
            // 计算当前索引指向数字的和
            int curSum = array[ahead] + array[behind];
            // 判断是否满足要求
            if (curSum == sum) {
                list.add(array[behind]);
                list.add(array[ahead]);
                break;
            } else if (curSum > sum) {
                ahead--;
            } else
                behind++;
        }
        return list;
    }
}

题目二:和为s的连续正数序列

输入一个正数sum,打印出所有和为sum的连续正数序列(至少包含两个数)。例如,输入15,由于1+2+3+4+5=4+5+6=7+8=15,所以打印出三个连续序列1到5,4到6和7到8.

→ 牛客网在线评判

题目详解

解法:

思路:用两个数small和big分别表示序列的最小值和最大值。首先把small初始化为1,big初识化为2;如果从small到big的序列的和大于sum,则从序列中去掉较小值,即增大small的值;如果从small到big的序列的和小于sum,则增大big。因为序列至少要有两个数字,small一直增大到(1+sum)/2为止。

import java.util.ArrayList;

public class Main {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> resultList = new ArrayList<>();
        if (sum < 3)
            return resultList;
        // 设置查找起点位置
        int small = 1;
        int big = 2;
        // 设置终止查找条件
        int mid = (1 + sum) / 2;
        // 计算当前数字和
        int curSum = small + big;
        while (small < mid) {
            // 如果当前和等于sum,将序列添加到链表中
            if (curSum == sum)
                addSequenceToList(resultList, small, big);
            // 将small增大,继续查找序列并添加
            while (curSum > sum && small < mid) {
                curSum -= small;
                small++;
                if (curSum == sum)
                    addSequenceToList(resultList, small, big);
            }
            // 将big增大,继续查找序列并添加
            big++;
            curSum += big;
        }
        return resultList;
    }
    // 添加序列到链表中
    private void addSequenceToList(ArrayList<ArrayList<Integer>> resultList, int small, int big) {
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = small; i <= big; i++) {
            list.add(i);
        }
        resultList.add(list);
    }
}

58. 翻转字符串

题目一:翻转单词顺序字

输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。简单起见,标点符号和普通字符一样处理。

→ 牛客网在线评判

题目详解

解法:

思路:第一步,翻转句子中所有的字符;第二步,翻转每个单词中字符的顺序。

public class Main {
    public String ReverseSentence(String str) {
        if (str == null)
            return null;
        StringBuilder sb = new StringBuilder(str);
        int length = sb.length();
        // 先翻转整个句子
        revesrse(sb, 0, length - 1);
        // 确定单词起始位置
        int pBegin = 0;
        int pEnd = 0;
        while (pBegin < length) {
            if (sb.charAt(pBegin) == ' ') {
                pBegin++;
                pEnd++;
            // 再将单词翻转为正常顺序
            } else if (pEnd == length || sb.charAt(pEnd) == ' ' ) {
                revesrse(sb, pBegin, --pEnd);
                pBegin = ++pEnd;
            } else
                pEnd++;
        }
        return sb.toString();
    }
    // 翻转部分字符串方法
    private void revesrse(StringBuilder sb, int pBegin, int pEnd) {
        while (pBegin < pEnd) {
            char tmp = sb.charAt(pBegin);
            sb.setCharAt(pBegin, sb.charAt(pEnd));
            sb.setCharAt(pEnd,tmp);
            pBegin++;
            pEnd--;
        }
    }
}

题目二:左旋转字符串

字符串的左旋转操作时把字符串前面的若干个字符串转移到字符串的尾部。例如,输入字符串"abcdefg"和数字2,返回左旋转两位的结果"cdefgad"。

→ 牛客网在线评判

题目详解

解法:

思路:将字符串分为两部分,前n个为第一部分,后面为另一部分,分别翻转这两部分;之后翻转整个字符串即为所求。

public class Main {
    public String LeftRotateString(String str,int n) {
        StringBuilder sb = new StringBuilder(str);
        if (str != null) {
            int length = sb.length();
            if (length > 0 && n > 0 && n < length) {
                // 先旋转前n个字符
                revesrse(sb, 0, n - 1);
                // 旋转后面字符
                revesrse(sb, n, length - 1);
                // 旋转整个字符串
                revesrse(sb, 0, length - 1);
            }
        }
        return sb.toString();
    }
    // 翻转部分字符串方法
    private void revesrse(StringBuilder sb, int pBegin, int pEnd) {
        while (pBegin < pEnd) {
            char tmp = sb.charAt(pBegin);
            sb.setCharAt(pBegin, sb.charAt(pEnd));
            sb.setCharAt(pEnd,tmp);
            pBegin++;
            pEnd--;
        }
    }
}

59. 队列的最大值

题目一:滑动窗口的最大值

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

→ 牛客网在线评判

题目详解

解法:

思路:利用双向队列查找滑动窗口的最大值,并添加到list中。每扫描到一个数字,将小于当前数字的值从队列中删除,保留当前大于当前值的相关值。

import java.util.ArrayList;
import java.util.LinkedList;

public class Main {
    public ArrayList<Integer> maxInWindows(int [] num, int size) {
        ArrayList<Integer> list = new ArrayList<>();
        int length = num.length;
        if (length >= size && size >= 1) {
            // 创建双向队列
            LinkedList<Integer> index = new LinkedList<>();
            // 对第一个窗口查找最大值
            for (int i = 0; i < size; i++) {
                // 删除队列中小于当前值的索引
                while (!index.isEmpty() && num[i] >= num[index.getLast()])
                    index.removeLast();
                // 添加当前索引
                index.addLast(i);
            }
            // 对之后的窗口查找最大值
            for (int i = size; i < length; i++) {
                // 添加最大值到list中
                list.add(num[index.getFirst()]);
                while (!index.isEmpty() && num[i] >= num[index.getLast()])
                    index.removeLast();
                // 如果最大值在窗口之外,删去最大值
                if (!index.isEmpty() && index.getFirst() <= (i - size))
                    index.removeFirst();
                index.addLast(i);
            }
            list.add(num[index.getFirst()]);
        }
        return list;
    }
}

题目二:队列的最大值

定义一个队列并实现函数max得到队列里的最大值,要求函数max、pushBack和popFront的时间复杂度都为O(1).

题目详解

解法:

思路:利用上题的思路,创建一个双向队列保存当前最大值。

import java.util.LinkedList;

public class QueueWithMax {
    // 封装数据的值和索引
    private class InternalDate{
        int number;
        int index;
        InternalDate(int number, int index){
            this.number = number;
            this.index = index;
        }
    }
    // 利用两个双向队列保存数据和最大值
    private LinkedList<InternalDate> data = new LinkedList<>();
    private LinkedList<InternalDate> max = new LinkedList<>();
    int currentIndex = 0;
    // 插入方法
    public void pushBack(int number) {
        while (!max.isEmpty() && number >= max.getLast().number)
            max.removeLast();
        InternalDate internalDate = new InternalDate(number,currentIndex);
        data.addLast(internalDate);
        max.addLast(internalDate);
        currentIndex++;
    }
    // 删除方法
    public void popFront() throws Throwable {
        if (max.isEmpty())
            throw new Throwable("queue is empty");

        if (max.getFirst().index == data.getFirst().index)
            max.removeFirst();

        data.removeFirst();
    }
    // 获得最大值
    public int max() throws Throwable {
        if (max.isEmpty())
            throw new Throwable("queue of empty");
        return max.getFirst().number;
    }
}

60. n个骰子的点数

题目:

把n个骰子扔在地上,所有的骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

题目详解

解法:

思路:同过分析,设定n个骰子的所有可能值为f(n),有:

f(n) = f(n-1)+f(n-2)+f(n-3)+f(n-4)+f(n-5)+f(n-6)
import java.util.ArrayList;

public class Main {
    public ArrayList<Double> printProbability(int number) {
        ArrayList<Double> list = new ArrayList<>();
        if (number < 1)
            return list;
        int gMaxValue = 6;
        // 创建数组保存中间值
        int[][] pProbabilities = new int[2][gMaxValue * number + 1];
        int flag = 0;
        // 初始化第一个数组1到6值为1
        for (int i = 1; i <= gMaxValue; i++)
            pProbabilities[flag][i] = 1;
        // 计算number个骰子次数情况
        for (int i = 2; i <= number; i++) {
            // 清空第二个数组前number的值
            for (int j = 0; j < i; j++)
                pProbabilities[1 - flag][j] = 0;
            // 通过公式f(n) = f(n-1)+f(n-2)+f(n-3)+f(n-4)+f(n-5)+f(n-6)计算f(n)
            for (int j = i; j <= i * gMaxValue; j++) {
                pProbabilities[1 - flag][j] = 0;
                for (int k = 1; k <= j && k <= gMaxValue ; k++)
                    pProbabilities[1 - flag][j] += pProbabilities[flag][j - k];
            }
            // 交换数组
            flag = 1 - flag;
        }
        // 计算概率
        double total = Math.pow((double)gMaxValue,number);
        for (int i = number; i <= number * gMaxValue; i++) {
            double ratio = (double)pProbabilities[flag][i]/total;
            list.add(ratio);
        }
        return list;
    }
}

61. 扑克牌中的顺子

题目:

从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2到10为数字本身,A为1,J为11,Q为12,K为13,而大小王可以看做任何数字。

→ 牛客网在线评判

题目详解

解法:

思路:首先把数组排序;其次统计数组中0的个数;再统计数组中相邻数字之间的空缺总数;最后比较两者大小,如果空缺总数小于或者等于0的个数,那么这个数组就是连续的。同时如果数组中有非零数字重复出现,则一定不是连续的。

import java.util.Arrays;

public class Main {
    public boolean isContinuous(int [] numbers) {
        int length = numbers.length;
        if (numbers == null || length < 1)
            return false;
        // 数组排序
        Arrays.sort(numbers);
        int numberOfZero = 0;
        int numberOfGap = 0;
        // 统计0的个数
        for (int i = 0; i < length && numbers[i] == 0 ; i++)
           numberOfZero++;
        // 统计相邻数字之间的空缺总数
        int small = numberOfZero;
        int big = small + 1;
        while (big < length) {
            // 如果存在对子,则不为顺子
            if (numbers[small] == numbers[big])
                return false;
            numberOfGap += numbers[big] - numbers[small] - 1;
            small = big;
            ++big;
        }
        // 如果空缺总数小于或者等于0的个数,则是顺子
        return (numberOfGap > numberOfZero) ? false : true;
    }
}

62. 圆圈中最后剩下的数字

题目:

0,1,...,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈剩下的最后一个数字。

→ 牛客网在线评判

题目详解

解法:

思路:通过数学分析可以得到递推公式:

f(n,m)= \begin{cases} 0, & \text {n=1} \\ (f(n-1,m)+m)modn, & \text{n>1} \end{cases} 

时间复杂度:O(n) 空间复杂度:O(1)

public class Main {
    public int LastRemaining_Solution(int n, int m) {
        if (n < 1 || m < 1)
            return -1;
        int last  = 0;
        // 根据递推公式求最后一个数字
        for (int i = 2; i <= n ; i++)
            last = (last + m) % i;
        return last;
    }
}

63. 股票的最大利润

题目:

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?例如,一只股票在某些时间节点的价格为{9,11,8,5,7,12,16,14}。如果我们在价格为5的时候买入并在价格为16的时候卖出,则能获得最大的利润11.

题目详解

解法:

思路:定义diff(i)为当卖出价为数组中第i个数字时可能获得的最大利润。当卖出价固定时,买入价越低获得的利润越大,只要获得前i-1个数字的最小值,即可以算出最大利润。 时间复杂度:O(n)

public class Main {
    public int maxDiff(int[] numbers) {
        int length = numbers.length;
        if (numbers == null || length < 2)
            return 0;
        // 创建变量保存最小值和最大差值
        int min = numbers[0];
        int maxDiff = numbers[1] - min;
        for (int i = 2; i < length; i++) {
            // 更新最小值
            if (numbers[i - 1] < min)
                min = numbers[i - 1];
            // 更新最大差值
            int currentDiff = numbers[i] - min;
            if (currentDiff > maxDiff)
                maxDiff = currentDiff;
        }
        return maxDiff;
    }
}

64. 求1+2+...+n

题目:

求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

→ 牛客网在线评判

题目详解

解法:

思路:利用逻辑与的短路特性实现递归终止。当n=0时只执行前面的终止判断,若为false,然后直接返回0;当n>0时,,实现递归计算。

public class Main {
    // 利用逻辑与的短路特性实现递归终止
    // 当n==0时,(n>0)&&((sum+=Sum_Solution(n-1))>0)只执行前面的判断,为false,然后直接返回0;
    // 当n>0时,执行sum+=Sum_Solution(n-1),实现递归计算Sum_Solution(n)。
    public int Sum_Solution(int n) {
        int sum = n;
        boolean ans = (n > 0) && ((sum += Sum_Solution(n - 1)) > 0);
        return sum;
    }
}

65. 不用加减乘除做加法

题目:

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

→ 牛客网在线评判

题目详解

解法:

思路:第一步不考虑进位对每一位相加,利用异或进行运算;第二步考虑进位,对每一位进行与运算并左移一位;第三步,继续重复前两步指导进位为0。

public class Main {
    public int Add(int num1,int num2) {
        int sum, carry;
        do {
            // 不考虑进位的每一位相加
            sum = num1 ^ num2;
            // 计算进位
            carry = (num1 & num2) << 1;
            // 重复相加,直到进位为0
            num1 = sum;
            num2 = carry;
        } while (num2 != 0);
        return num1;
    }
}

66. 构建乘积数组

题目:

给定一个数组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]。不能使用除法。

→ 牛客网在线评判

题目详解

解法:

思路:定义B[i]=C[i]D[i],其中C[i]=A[0]A[1]...A[i-1],D[i]=A[i+1]...A[n-2]A[n-1]。C[i]可以用自上而下的顺序计算出来,即C[i]=c[i-1]A[i-1];D[i]可以通过自下而上的顺序计算出来,即D[i]=D[i+1]*A[i+1]。 时间复杂度:O(n)

public class Main {
    public int[] multiply(int[] A) {
        int length = A.length;
        int[] array = new int[length];
        if (length > 1) {
            array[0] = 1;
            // 计算前一半的值A[0]*A[1]*...*A[i-1]
            for (int i = 1; i < length; i++) {
                array[i] = array[i - 1] * A[i - 1];
            }
            // 计算后一半的值A[i+1]*...*A[n-2]*A[n-1]
            int temp = 1;
            for (int i = length - 2; i >= 0 ; i--) {
                temp *= A[i + 1];
                array[i] *= temp;
            }
        }
        return array;
    }
}

67. 把字符串转换成整数

题目:

将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。

→ 牛客网在线评判

题目详解

解法:

思路:首先进行符号判断,之后将每一位转换为数字并求和,最后根据符号输出。

public class Main {
    public int StrToInt(String str) {
        int length = str.length();
        if (str == null || length < 1)
            return 0;
        // 判断是否为负数
        boolean minus = str.charAt(0) == '-';
        int num = 0;
        // 计算数字大小
        for (int i = 0; i < length ; i++) {
            char c = str.charAt(i);
            // 跳过符号
            if (i == 0 && (c == '+' || c == '-'))
                continue;
            // 处理非法字符
            if (c < '0' || c > '9')
                return 0;
            num = num * 10 + (c - '0');
        }
        return minus ? -num : num;
    }
}

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

题目一:二叉搜索树求最低公共祖先

对于一个二叉搜索树,输入两个树节点,求它们的最低公共祖先。

题目详解

解法:

思路:二叉搜索树是排序过的,左子树的节点都比父节点小,右子树的节点都比父节点大。从根节点开始和两个节点进行对比,如果当前节点比两个节点大,则最低公共祖先在当前节点的左子树,下一步遍历左子节点;如果当前节点比两个节点小,则最低公共祖先在当前节点的右子树,下一步遍历右子节点;如果介于中间,则为最低公共祖先。

public class Main {
    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;
    }
}

题目二:普通二叉树求最低公共祖先

对于一个二叉树,输入两个树节点,求它们的最低公共祖先。

题目详解

解法:

思路:在左右子树中查找两个节点的最低公共祖先,如果在其中一颗子树中查找到,那么就返回这个解,否则可以认为根节点就是最低公共祖先。

public class Main {
    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;
    }
}

参考文献

  • 何海涛. 剑指 Offer[M]. 电子工业出版社, 2017.5,第二版

转载于:https://www.cnblogs.com/apollospotatolikett/p/9109442.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值