《剑指offer》思路及Java实现

文章目录


高亮为面试高频题

1、二维数组中的查找

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

思路:

矩阵是有序的,从右上角来看,向左数字递减,向下数字递增。

因此从右上角开始查找,当右上角的数字比查找的数字大时,左移;当右上角数字比查找的数字小时,下移。

public class Solution {
    public boolean Find(int target, int[][] array) {
        int row = 0;
        int col = array[0].length - 1;
        while (row < array.length && col >= 0) {
            if (array[row][col] == target) {
                return true;
            } else if (array[row][col] > target) {
                col--;
            } else {
                row++;
            }
        }
        return false;
    }
}
2、替换字符串中的空格

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

思路:

从后往前,在原来的字符串上进行替换。

public class Solution {
    public String replaceSpace(StringBuffer str) {
        int spaceNum = 0; // 计算空格数
        int len = str.length();
        for (int i = 0; i < len; i++) {
            if (str.charAt(i) == ' ') {
                spaceNum++;
            }
        }
        int newLen = len + spaceNum * 2;
        str.setLength(newLen);
        for (int i = len - 1; i >= 0; i--) {
            if (str.charAt(i) == ' ') {
                str.setCharAt(--newLen, '0');
                str.setCharAt(--newLen, '2');
                str.setCharAt(--newLen, '%');
            } else {
                str.setCharAt(--newLen, str.charAt(i));
            }
        }
        return str.toString();
    }
}
3、从尾到头打印链表

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

思路:

方法一:递归实现

/**
*    public class ListNode {
*        int val;
*        ListNode next = null;
*
*        ListNode(int val) {
*            this.val = val;
*        }
*    }
*/
import java.util.ArrayList;

public class Solution {
    ArrayList<Integer> list = new ArrayList<>();

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

**方法二:**借助栈的“先进后出”实现

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

public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        Stack<Integer> s = new Stack<>();
        ArrayList<Integer> list = new ArrayList<>();
        while (listNode != null) {
            s.push(listNode.val);
            listNode = listNode.next;
        }
        while (!s.isEmpty()) {
            list.add(s.pop());
        }
        return list;
    }
}
4、由前序和中序遍历重建二叉树

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

思路:

直接看代码吧

/**
 * Definition for binary tree
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        TreeNode root = reConstructBinaryTree(pre, 0, pre.length-1, 
                                              in, 0, in.length-1);
        return root;
    }
     
    private TreeNode reConstructBinaryTree(int[] pre, int startPre, int endPre, 
    									   int[] in, int startIn, int endIn) {
        if (startPre > endPre || startIn > endIn) {
            return null;
        }
         
        TreeNode root = new TreeNode(pre[startPre]); // 前序遍历的第一个数字是根节点
        for (int i = startIn; i <= endIn; i++) { // 在中序遍历中找到根节点的值
            if (in[i] == pre[startPre]) {
                // 构建左子树
                root.left = 
                    reConstructBinaryTree(pre, startPre+1, (i-startIn)+startPre, 
                                          in, startIn, i-1);
                // 构建右子树
                root.right = 
                    reConstructBinaryTree(pre, (i-startIn)+startPre+1, endPre, 
                                          in, i+1, endIn);
            }
        }
        return root;
    }
}
5、用两个栈实现队列

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

思路:

import java.util.Stack;

public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();

    public void push(int node) {
        stack1.push(node);
    }

    public int pop() throws Exception {
        if (stack2.isEmpty()) {
            while (!stack1.isEmpty()) {
                stack2.push(stack1.pop());
            }
        }
        if (stack2.isEmpty()) {
            throw new Exception("queue is empty!");
        }
        return stack2.pop();
    }
}
6、旋转数组的最小数字

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

思路:

链接:

采用二分法解答这个问题: m i d = ( l o w + h i g h ) / 2 mid = (low + high) / 2 mid=(low+high)/2

需要考虑三种情况:

  1. array[mid] > array[high]:

出现这种情况的array类似[3,4,5,6,0,1,2],此时最小数字一定在mid的右边。

l o w = m i d + 1 low = mid + 1 low=mid+1

  1. array[mid] == array[high]:

出现这种情况的array类似 [1,0,1,1,1] 或者[1,1,1,0,1],此时最小数字不好判断在mid左边

还是右边,这时只好一个一个试 ,

h i g h = h i g h − 1 high = high - 1 high=high1

  1. array[mid] < array[high]:

出现这种情况的array类似[2,2,3,4,5,6,6],此时最小数字一定就是array[mid]或者在mid的左边。因为右边必然都是递增的。

h i g h = m i d high = mid high=mid

注意这里有个坑:如果待查询的范围最后只剩两个数,那么mid 一定会指向下标靠前的数字

比如 array = [4,6]

array[low] = 4 ;array[mid] = 4 ; array[high] = 6 ;

如果 h i g h = m i d − 1 high = mid - 1 high=mid1,就会产生错误, 因此 h i g h = m i d high = mid high=mid

但情形1中 l o w = m i d + 1 low = mid + 1 low=mid+1就不会错误

public class Solution {
    public int minNumberInRotateArray(int[] array) throws Exception {
        if (array == null || array.length == 0) {
            throw new Exception("Invalid parameters!");
        }

        int low = 0;
        int high = array.length - 1;
        int mid = 0;

        while (low < high) {
            mid = (low + high) / 2;
            if (array[low] == array[high]) {
                high--;
            } else if (array[mid] > array[high]) {
                low = mid + 1;
            } else { // array[mid] < array[high],最小值可能为mid,或在mid左边
                high = mid;
            }
        }
        return array[high];
    }
}
7、斐波那契数列

大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。

n<=39

思路:

  1. 递归方法(最差的方法)
public class Solution {
    public int Fibonacci(int n) {
        if (n < 2) {
            return n;
        }
        return Fibonacci(n - 1) + Fibonacci(n - 2);
    }
}
  1. 动态规划:主要应用于解决子问题重叠的情况

    简单来说,就是:“记住求过的解来节省时间”

public class Solution {
    public int Fibonacci(int n) {
        if (n < 2) {
            return n;
        }
        
        int[] arr = new int[n + 1];
        arr[0] = 0;
        arr[1] = 1;
        for (int i = 2; i <= n; i++) {
            arr[i] = arr[i - 1] + arr[i - 2];
        }
        return arr[n];
    }
}
  1. 尾递归
public class Solution {
    public int Fibonacci(int n) {
        return Fibonacci(n, 0, 1);
    }
 
    public int Fibonacci(int n, int acc1, int acc2) {
        if (n == 0)
            return acc1;
        if (n == 1)
            return acc2;
        return Fibonacci(n - 1, acc2, acc1 + acc2);
    }
}
8、跳台阶

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

思路:

跳到第n个台阶,只有两种可能:

  1. 从第n-1个台阶跳1个台阶
  2. 从第n-2个台阶跳2个台阶

最终可得出:
(1) f ( n ) = { 1 n = 1 2 n = 2 f ( n − 1 ) + f ( n − 2 ) n &gt; 2 且 n 为 整 数 f(n) = \begin{cases} 1 &amp; n = 1 \\ 2 &amp; n = 2 \\ f(n - 1) + f(n - 2) &amp; n &gt; 2且n为整数 \end{cases} \tag{1} f(n)=12f(n1)+f(n2)n=1n=2n>2n(1)

public class Solution {
    public int JumpFloor(int target) {
        if (target < 3) {
            return target;
        }
        
        int[] jump = new int[target + 1];
        jump[0] = 0;
        jump[1] = 1;
        jump[2] = 2;
        for (int i = 3; i <= target; i++) {
            jump[i] = jump[i - 1] + jump[i - 2];
        }
        return jump[target];
    }
}
9、变态跳台阶

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

思路:

  1. 关于本题,前提是n个台阶会有一次n阶的跳法。分析如下:

    f(1) = 1

    f(2) = f(2-1) + f(2-2) //f(2-2) 表示2阶一次跳2阶的次数。

    f(3) = f(3-1) + f(3-2) + f(3-3)

    f(n) = f(n-1) + f(n-2) + f(n-3) + … + f(n-(n-1)) + f(n-n)

    说明:

    1)这里的f(n) 代表的是n个台阶有一次1,2,…n阶的 跳法数。

    2)n = 1时,只有1种跳法,f(1) = 1

    3)n = 2时,会有两个跳得方式,一次1阶或者2阶,这回归到了问题(1) ,f(2) = f(2-1) + f(2-2)

    4)n = 3时,会有三种跳得方式,1阶、2阶、3阶,

    ​ 那么就是第一次跳出1阶后面剩下:f(3-1);第一次跳出2阶,剩下f(3-2);第一次3阶,那么剩下f(3-3)

    ​ 因此结论是f(3) = f(3-1)+f(3-2)+f(3-3)

    5)n = n时,会有n中跳的方式,1阶、2阶…n阶,得出结论:

    ​ f(n) = f(n-1)+f(n-2)+…+f(n-(n-1)) + f(n-n) => f(0) + f(1) + f(2) + f(3) + … + f(n-1)

    1. 由以上已经是一种结论,但是为了简单,我们可以继续简化:

    ​ f(n-1) = f(0) + f(1)+f(2)+f(3) + … + f((n-1)-1) = f(0) + f(1) + f(2) + f(3) + … + f(n-2)

    ​ f(n) = f(0) + f(1) + f(2) + f(3) + … + f(n-2) + f(n-1) = f(n-1) + f(n-1)

    ​ 可以得出:

    ​ f(n) = 2*f(n-1)

    1. 得出最终结论,在n阶台阶,一次有1、2、…n阶的跳的方式时,总得跳法为:
      (1) f ( n ) = { 1 n = 0 , 1 2 ∗ f ( n − 1 ) n ≥ 2 f(n) = \begin{cases} 1 &amp; n = 0,1 \\ 2 * f(n - 1) &amp; n \geq 2 \end{cases} \tag{1} f(n)={12f(n1)n=0,1n2(1)
public class Solution {
    public int JumpFloorII(int target) {
        if (target <= 0) {
            return -1;
        } else if (target == 1) {
            return 1;
        } else {
            return 2 * JumpFloorII(target - 1);
        }
    }
}
  1. 除了最后一个台阶青蛙必须停留,其他台阶上都有青蛙停留或不停留两种情况,因此总数为2^(n-1)次。

    第二种思想更简单一些。

public class Solution {
    public int JumpFloorII(int target) {
        if (target <= 0)
            return 0;
        return (int) Math.pow(2, target-1);
    }
}
10、矩形覆盖

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

思路:

依旧是斐波那契数列

2*n的大矩形,和n个2*1的小矩形

其中target*2为大矩阵的大小

有以下几种情形:

  1. target <= 0 大矩形为<= 2*0,直接return 1;

  2. target = 1大矩形为2*1,只有一种摆放方法,return1;

  3. target = 2 大矩形为2*2,有两种摆放方法,return2;

  4. target = n 分为两步考虑:

    第一次摆放一块 2*1 的小矩阵,则摆放方法总共为f(target - 1)

第一次摆放一块1*2的小矩阵,则摆放方法总共为f(target-2)

因为,摆放了一块1*2的小矩阵(用√√表示),对应下方的1*2(用××表示)摆放方法就确定了,所以为f(targte-2)

××
public class Solution {
    public int RectCover(int target) {
        if (target < 3) {
            return target;
        }
        int[] arr = new int[target + 1];
        arr[0] = 0;
        arr[1] = 1;
        arr[2] = 2;
        for (int i = 3; i <= target; i++) {
            arr[i] = arr[i - 1] + arr[i - 2];
        }
        return arr[target];
    }
}
11、二进制中1的个数

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

思路:

  1. 如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。

    举个例子:一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011。我们发现减1的结果是把最右边的一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。如1100&1011=1000。也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。

public class Solution {
    public static int NumberOf1(int n) {
        int count = 0;
        while (n != 0) {
            count++;
            n = n & (n - 1);
        }
        return count;
    }
}
  1. Integer.toBinaryString() 的用法:以二进制无符号整数形式返回一个整数参数的字符串表示形式
public class Solution {
    public static int NumberOf1(int n) {
        return Integer.toBinaryString(n).replaceAll("0", "").length();
    }
}
12、数值的整数次方

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

思路:

  1. 累乘法
public class Solution {
    public double Power(double base, int exponent) {
        double ans = 1;
        for (int i = 0; i < Math.abs(exponent); i++) {
            ans *= base;
        }
        return exponent > 0 ? ans : 1 / ans;
    }
}
  1. 快速幂
public class Solution {
    public double Power(double base, int exponent) {
        double ans = 1;
        int e = exponent > 0 ? exponent : -1 * exponent;
        while (e != 0) {
            if ((e & 1) == 1) {
                ans *= base;
            }
            base *= base;
            e = e >> 1;
        }
        return exponent > 0 ? ans : 1 / ans;
    }
}
13、调整数组顺序,使奇数位于偶数前面

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

思路:

  1. 创建一个新数组
public class Solution {
    public void reOrderArray(int [] array) {
        int oddNum = 0;
        for (int i = 0; i < array.length; i++) {
            if ((array[i] & 1) == 1) {
                oddNum++;
            }
        }
        int[] arr = new int[array.length];
        int odd = 0, even = oddNum;
        for (int i = 0; i < array.length; i++) {
            if ((array[i] & 1) == 1) {
                arr[odd++] = array[i];
            } else {
                arr[even++] = array[i];
            }
        }
        for (int i = 0; i < array.length; i++) {
            array[i] = arr[i];
        }
    }
}
  1. 冒泡法:前偶后奇就交换,直到没有可交换的数字
public class Solution {
    public void reOrderArray(int [] array) {
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array.length - 1; j++) {
                if ((array[j] & 1) == 0 && (array[j + 1] & 1) == 1) {
                    int tem = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = tem;
                }
            }
        }
    }
}
14、链表中倒数第k个结点

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

思路:

两个指针,先让第一个指针和第二个指针都指向头结点,然后再让第一个指正走(k-1)步,到达第k个节点。然后两个指针同时往后移动,当第一个结点到达末尾的时候,第二个结点所在位置就是倒数第k个节点了。

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

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode FindKthToTail(ListNode head,int k) {
        if (head == null || k <= 0) {
            return null;
        }
        ListNode pre = head; // 上一个节点
        ListNode last = head; // 计数节点
        for (int i = 1; i < k; i++) {
            if (last.next != null) {
                last = last.next;
            } else {
                return null;
            }
        }
        while (last.next != null) {
            last = last.next;
            pre = pre.next;
        }
        return pre;
    }
}
15、反转链表

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

思路:

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

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode ReverseList(ListNode head) {
        //head为当前节点,如果当前节点为空的话,那就什么也不做,直接返回null;
        if (head == null) {
            return null;
        }
        //当前节点是head,pre为当前节点的前一节点,next为当前节点的下一节点
        //需要pre和next的目的是让当前节点从pre->head->next1->next2变成pre<-head next1->next2
        //即pre让节点可以反转所指方向,但反转之后如果不用next节点保存next1节点的话,此单链表就此断开了
        //所以需要用到pre和next两个节点
        ListNode pre = null;
        ListNode next = null;
        //1->2->3->4->5
        //1<-2<-3 4->5
        while (head != null) {
            //做循环,如果当前节点不为空的话,始终执行此循环,此循环的目的就是让当前节点从指向next到指向pre
            //如此就可以做到反转链表的效果
            //先用next保存head的下一个节点的信息,保证单链表不会因为失去head节点的原next节点而就此断裂
            next = head.next;
            //保存完next,就可以让head从指向next变成指向pre了,代码如下
            head.next = pre;
            //head指向pre后,就继续依次反转下一个节点
            //让pre,head,next依次向后移动一个节点,继续下一次的指针反转
            pre = head;
            head = next;
        }
        //如果head为null的时候,pre就为最后一个节点了,但是链表已经反转完毕,pre就是反转后链表的第一个节点
        //直接输出pre就是我们想要得到的反转后的链表
        return pre;
    }
}
16、合并两个有序链表

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

思路:

  1. 非递归版本
/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode Merge(ListNode list1,ListNode list2) {
        // 可有可无
//        if (list1 == null) {
//            return list2;
//        }
//        if (list2 == null) {
//            return list1;
//        }
        ListNode list = new ListNode(-1);
        ListNode head = list;
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                list.next = list1;
                list1 = list1.next;
            } else {
                list.next = list2;
                list2 = list2.next;
            }
            list = list.next;
        }
        if (list1 != null) {
            list.next = list1;
        }
        if (list2 != null) {
            list.next = list2;
        }
        return head.next;
    }
}
  1. 递归版本
/*
public class ListNode {
    int val;
    ListNode next = null;

    ListNode(int val) {
        this.val = val;
    }
}*/
public class Solution {
    public ListNode Merge(ListNode list1,ListNode list2) {
        if (list1 == null) {
            return list2;
        }
        if (list2 == null) {
            return list1;
        }
        if (list1.val < list2.val) {
            list1.next = Merge(list1.next, list2);
            return list1;
        } else {
            list2.next = Merge(list1, list2.next);
            return list2;
        }
    }
}
17、树的子结构(判断二叉树A中是否包含子树B)

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

思路:

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

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public boolean HasSubtree(TreeNode root1, TreeNode root2) {
        boolean result = false;
        if (root1 != null && root2 != null) {
            if (root1.val == root2.val) {
                result = doseTree1HaveTree2(root1, root2);
            }
            if (!result) {
                result = HasSubtree(root1.left, root2);
            }
            if (!result) {
                result = HasSubtree(root1.right, root2);
            }
        }
        return result;
    }

    private boolean doseTree1HaveTree2(TreeNode root1, TreeNode root2) {
        if (root2 == null) {
            return true;
        }
        if (root1 == null) {
            return false;
        }
        if (root1.val != root2.val) {
            return false;
        }
        return doseTree1HaveTree2(root1.left, root2.left) 
            && doseTree1HaveTree2(root1.right, root2.right);
    }
}
18、二叉树的镜像

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

输入描述:

二叉树的镜像定义:源二叉树 
    	    8
    	   /  \
    	  6   10
    	 / \  / \
    	5  7 9 11
    	镜像二叉树
    	    8
    	   /  \
    	  10   6
    	 / \  / \
    	11 9 7  5

思路:

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

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public void Mirror(TreeNode root) {
        if (root != null) {
            TreeNode temp = root.left;
            root.left = root.right;
            root.right = temp;

            Mirror(root.left);
            Mirror(root.right);
        }
    }
}
19、顺时针打印矩阵

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

思路:

import java.util.ArrayList;

public class Solution {
    public ArrayList<Integer> printMatrix(int [][] matrix) {
        int row = matrix.length;
        int col = matrix[0].length;
        ArrayList<Integer> list = new ArrayList<>();
        int circle = ((row < col ? row : col) + 1) / 2;
        for (int c = 0; c < circle; c++) { // 第c圈
            // 从左到右:行不变(c),列变
            for (int j = c; j < col - c; j++) {
                list.add(matrix[c][j]);
            }
            // 从上到下:行变,列不变(col-c)
            for (int i = c + 1; i < row - c; i++) {
                list.add(matrix[i][col - c - 1]);
            }
            // 从右到左:行不变(row-c),列变
            for (int j = col - c - 2; j >= c && row - c - 1 > c; j--) {
                list.add(matrix[row - c - 1][j]);
            }
            // 从下到上:行变,列不变(c)
            for (int i = row - c - 2; i > c && c < col - c - 1; i--) {
                list.add(matrix[i][c]);
            }
        }
        return list;
    }
}
20、包含min函数的栈

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

思路:

import java.util.Stack;

public class Solution {
    Stack<Integer> data = new Stack<>();
    Stack<Integer> min = new Stack<>();

    public void push(int node) {
        data.push(node);
        if (min.empty()) {
            min.push(node);
        } else {
            min.push(min.peek() < node ? min.peek() : node);
        }
    }

    public void pop() {
        if (!data.empty()) {
            data.pop();
            min.pop();
        }
    }

    public int top() {
        return data.peek();
    }

    public int min() {
        return min.peek();
    }
}
21、栈的压入、弹出序列(判断一个栈是否是另一个栈的弹出序列)

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

思路:

设置一个辅助栈判断popA是否合理。

import java.util.Stack;

public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
      boolean result = false;
        Stack<Integer> s = new Stack<>();
        int push = 0, pop = 0;
        for (; push < pushA.length; push++) {
            s.push(pushA[push]);
            while (!s.empty() && s.peek() == popA[pop]) {
                pop++;
                s.pop();
            }
        }
        if (s.empty() && pop == popA.length) {
            result = true;
        }
        return result;
    }
}
22、从上往下打印二叉树(层次遍历)

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

思路:

二叉树的层次遍历

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

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ArrayList<Integer> PrintFromTopToBottom(TreeNode root) {
        ArrayList<Integer> result = new ArrayList<>();
        if (root == null) {
            return result;
        }
        ArrayList<TreeNode> list = new ArrayList<>();
        list.add(root);
        while (!list.isEmpty()) {
            TreeNode node = list.remove(0);
            result.add(node.val);
            if (node.left != null) {
                list.add(node.left);
            }
            if (node.right != null) {
                list.add(node.right);
            }
        }
        return result;
    }
}
23、二叉搜索树的后序遍历序列

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

思路:

public class Solution {
    public boolean VerifySquenceOfBST(int[] sequence) {
        if (sequence == null || sequence.length == 0) {
            return false;
        }
        return helper(sequence, 0, sequence.length - 1);
    }

    private boolean helper(int[] sequence, int start, int end) {
        int mid = start;
        for (; mid < end; mid++) {
            if (sequence[mid] > sequence[end]) {
                break;
            }
        }
        for (int i = mid; i < end; i++) {
            if (sequence[i] < sequence[end]) {
                return false;
            }
        }
        boolean left = true;
        if (mid > start) {
            left = helper(sequence, start, mid - 1);
        }
        boolean right = true;
        if (mid < end - 1) {
            right = helper(sequence, mid, end - 1);
        }
        return left && right;
    }
}
24、二叉树中和为某一值的路径

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

思路:

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

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    ArrayList<ArrayList<Integer>> result = new ArrayList<>();
    ArrayList<Integer> list = new ArrayList<>();

    public ArrayList<ArrayList<Integer>> FindPath(TreeNode root, int target) {
        if (root == null) {
            return result;
        }
        list.add(root.val);
        target -= root.val;
        if (target == 0 && root.left == null && root.right == null) {
            // ArrayList是引用数据类型。
            // 如果直接添加list,之后对list的操作还会改变result中list的值
            result.add(new ArrayList<Integer>(list));
        } else {
            FindPath(root.left, target);
            FindPath(root.right, target);
        }
        list.remove(list.size() - 1);
        return result;
    }
}
25、复杂链表的复制

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

思路:

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

    RandomListNode(int label) {
        this.label = label;
    }
}
*/
public class Solution {
    public RandomListNode Clone(RandomListNode pHead) {
        if (pHead == null) {
            return null;
        }
        // 1.在原始链表中,复制新节点
        RandomListNode node = pHead;
        while (node != null) {
            RandomListNode newNode = new RandomListNode(node.label);
            newNode.next = node.next;
            node.next = newNode;
            node = newNode.next;
        }
        // 2.设置新节点的random指针
        node = pHead;
        while (node != null) {
            if (node.random != null) {
                node.next.random = node.random.next;
            }
            node = node.next.next;
        }
        // 3.拆分新旧节点
        node = pHead;
        RandomListNode newHead = pHead.next;
        RandomListNode newNode = newHead;
        while (node != null) {
            node.next = newNode.next;
            if (newNode.next != null) {
                newNode.next = newNode.next.next;
            }
            node = node.next;
            newNode = newNode.next;
        }
        return newHead;
    }
}
26、二叉搜索树与双向链表

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

思路:

按照中序遍历的思路对二叉搜索树进行转换。

  1. 递归方法
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public TreeNode Convert(TreeNode pRootOfTree) {
        if (pRootOfTree == null) {
            return null;
        }
        // 若为叶子节点
        if (pRootOfTree.left == null && pRootOfTree.right == null) {
            return pRootOfTree;
        }
        // 将左子树转变为双向链表,并保存左子树链表头结点
        TreeNode left = Convert(pRootOfTree.left);
        // 找到左子树链表的最右节点
        TreeNode last = left;
        while (last != null && last.right != null) {
            last = last.right;
        }
        // 将左子树链表的最右节点与根节点连起来
        if (last != null) {
            last.right = pRootOfTree;
            pRootOfTree.left = last;
        }
        // 将右子树转变为双向链表,并保存右子树链表头结点
        TreeNode right = Convert(pRootOfTree.right);
        // 将右子树的头结点与根节点连在一起
        if (right != null) {
            pRootOfTree.right = right;
            right.left = pRootOfTree;
        }
        // 若左子树为空,返回根节点;否则返回左子树头结点
        return left == null ? pRootOfTree : left;
    }
}
  1. 非递归方法
import java.util.Stack;

public class Solution {
    public TreeNode Convert(TreeNode pRootOfTree) {
        if (pRootOfTree == null) {
            return null;
        }
        Stack<TreeNode> s = new Stack<>();
        TreeNode node = pRootOfTree; // 将中序遍历序列中的第一个节点记为pRootOfTree
        TreeNode pre = null; // 用于保存中序遍历中的上一节点
        boolean isFirst = true;
        while (node != null || !s.empty()) {
            while (node != null) {
                s.push(node);
                node = node.left;
            }
            node = s.pop(); // 左孩子为空的根节点(即当前最左边的点)
            if (isFirst) {
                pRootOfTree = node;
                pre = node;
                isFirst = false;
            } else {
                pre.right = node;
                node.left = pre;
                pre = node;
            }
            node = node.right;
        }
        return pRootOfTree;
    }
}
27、字符串的排列

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

输入描述:

输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。

思路:

递归算法:

解析:字符串全排列算法学习

1、对于无重复值的情况

固定第一个字符,递归取得首位后面的各种字符串组合;

再把第一个字符与后面每一个字符交换,并同样递归获得首位后面的字符串组合;

递归的出口,就是只剩一个字符的时候,即i == ch.length - 1

递归的循环过程,就是从每个子串的第二个字符开始依次与第一个字符交换,然后继续处理子串。

2、假如有重复值呢?

在添加字符串到结果集时,进行判重

在这里插入图片描述

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

public class Solution {
    public ArrayList<String> Permutation(String str) {
        ArrayList<String> list = new ArrayList<>();
        char[] ch = str.toCharArray();
        if (str != null && str.length() != 0) {
            // 递归的初始值为(str数组形式,初始下标0,空的list)
            PermutationHelper(ch, 0, list);
            Collections.sort(list);
        }
        return list;
    }
 
    public void PermutationHelper(char[] ch, int i, ArrayList<String> list) {
        // 递归的终止条件:当i下标已经移到char数组的末尾的时候,判断是否添加这一组字符串进入结果集中
        if (i == ch.length - 1) {
            String str = String.valueOf(ch);
            // 判重
            if (!list.contains(str)) {
                list.add(str);
            }
        } else {
            for (int j = i; j < ch.length; j++) {
                swap(ch, i, j); // 依次与之后的字符进行交换
                PermutationHelper(ch, i + 1, list); // 递归处理之后部分的字符串
                swap(ch, i, j); // 复位
            }
        }
    }
 
    public void swap(char[] ch, int i, int j) {
        char temp = ch[i];
        ch[i] = ch[j];
        ch[j] = temp;
    }
}
28、数组中出现次数超过一半的数字

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

思路:

如果有符合条件的数字,则它出现的次数比其他所有数字出现的次数和还要多。

在遍历数组时保存两个值:一是数组中一个数字,一是次数。遍历下一个数字时,若它与之前保存的数字相同,则次数加1,否则次数减1;若次数为0,则保存下一个数字,并将次数置为1。遍历结束后,所保存的数字即为所求。然后再判断它是否符合条件即可。

public class Solution {
    public int MoreThanHalfNum_Solution(int [] array) {
        if (array == null || array.length == 0) {
            return 0;
        }
 
        // 将首次出现的数count置1。并与之后的数字进行比较,相等count+1;不等count-1
        int num = array[0];
        int count = 1;
        for (int i = 1; i < array.length; i++) {
            if (array[i] == num) {
                count++;
            } else {
                count--;
            }
            if (count == 0) {
                num = array[i];
                count = 1;
            }
        }
        // 检验出现次数最多的数字,次数是否大于总长度的一半
        count = 0;
        for (int i = 0; i < array.length; i++) {
            if (array[i] == num) {
                count++;
            }
        }
        if (count * 2 <= array.length) {
            num = 0;
        }
        return num;
    }
}
29、最小的K个数(TOP K问题)

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

思路:

用大根堆保存这k个数,每次只和堆顶比,如果比堆顶小,删除堆顶,新数入堆。

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

public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int[] input, int k) {
        ArrayList<Integer> result = new ArrayList<Integer>();
        int length = input.length;
        if (k > length || k == 0) {
            return result;
        }
        // 创建大根堆
        PriorityQueue<Integer> maxHeap = new PriorityQueue<Integer>(k, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        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;
    }
}
30、连续子数组的最大和

HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

思路:

使用动态规划

F(i):以array[i]为末尾元素的子数组的和的最大值,子数组的元素的相对位置不变

F ( i ) = m a x ( F ( i − 1 ) + a r r a y [ i ] , a r r a y [ i ] ) F(i)=max(F(i-1)+array[i],array[i]) F(i)=max(F(i1)+array[i],array[i])

res:所有子数组的和的最大值

r e s = m a x ( r e s , F ( i ) ) res=max(res,F(i)) res=max(res,F(i))

如数组[6, -3, -2, 7, -15, 1, 2, 2]

初始状态:

F ( 0 ) = 6 F(0) = 6 F(0)=6

r e s = 6 res = 6 res=6

i=1:

F ( 1 ) = m a x ( F ( 0 ) − 3 , − 3 ) = m a x ( 6 − 3 , 3 ) = 3 F(1)=max(F(0)-3,-3)=max(6-3,3)=3 F(1)=max(F(0)3,3)=max(63,3)=3

r e s = m a x ( F ( 1 ) , r e s ) = m a x ( 3 , 6 ) = 6 res=max(F(1),res)=max(3,6)=6 res=max(F(1),res)=max(3,6)=6

i=2:

F ( 2 ) = m a x ( F ( 1 ) − 2 , − 2 ) = m a x ( 3 − 2 , − 2 ) = 1 F(2)=max(F(1)-2,-2)=max(3-2,-2)=1 F(2)=max(F(1)2,2)=max(32,2)=1

r e s = m a x ( F ( 2 ) , r e s ) = m a x ( 1 , 6 ) = 6 res=max(F(2),res)=max(1,6)=6 res=max(F(2),res)=max(1,6)=6

i=3:

F ( 3 ) = m a x ( F ( 2 ) + 7 , 7 ) = m a x ( 1 + 7 , 7 ) = 8 F(3)=max(F(2)+7,7)=max(1+7,7)=8 F(3)=max(F(2)+7,7)=max(1+7,7)=8

r e s = m a x ( F ( 2 ) , r e s ) = m a x ( 8 , 6 ) = 8 res=max(F(2),res)=max(8,6)=8 res=max(F(2),res)=max(8,6)=8

i=4:

F ( 4 ) = m a x ( F ( 3 ) − 15 , − 15 ) = m a x ( 8 − 15 , − 15 ) = − 7 F(4)=max(F(3)-15,-15)=max(8-15,-15)=-7 F(4)=max(F(3)15,15)=max(815,15)=7

r e s = m a x ( F ( 4 ) , r e s ) = m a x ( − 7 , 8 ) = 8 res=max(F(4),res)=max(-7,8)=8 res=max(F(4),res)=max(7,8)=8

以此类推

最终res的值为8

public class Solution {
    public int FindGreatestSumOfSubArray(int[] array) {
        if (array == null || array.length == 0) {
            return 0;
        }
        int res = array[0]; // 当前所有子数组的和的最大值
        int max = array[0]; // 包含array[i]的连续数组最大值
        for (int i = 1; i < array.length; i++) {
            res = Math.max(res + array[i], array[i]);
            max = Math.max(max, res);
        }
        return max;
    }
}
31、整数中1出现的次数(从1到n整数中1出现的次数)

求出1 ~ 13的整数中1出现的次数,并算出100 ~ 1300的整数中1出现的次数?为此他特别数了一下1 ~ 13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

思路:

设N = abcde ,其中abcde分别为十进制中各位上的数字。

如果要计算百位上1出现的次数,它要受到3方面的影响:百位上的数字,百位以下(低位)的数字,百位以上(高位)的数字。

  1. 如果百位上数字为0,百位上可能出现1的次数由更高位决定。比如:12013,则可以知道百位出现1的情况可能是:100 ~ 199,1100 ~ 1199,2100 ~ 2199,……,11100 ~ 11199,一共1200个。可以看出是由更高位数字(12)决定,并且等于更高位数字(12)乘以 当前位数(100)。

  2. 如果百位上数字为1,百位上可能出现1的次数不仅受更高位影响还受低位影响。比如:12113,则可以知道百位受高位影响出现的情况是:100 ~ 199,1100 ~ 1199,2100 ~ 2199,……,11100 ~ 11199,一共1200个。和上面情况一样,并且等于更高位数字(12)乘以 当前位数(100)。但同时它还受低位影响,百位出现1的情况是:12100 ~ 12113,一共114个,等于低位数字(113)+1。

  3. 如果百位上数字大于1(2 ~ 9),则百位上出现1的情况仅由更高位决定,比如12213,则百位出现1的情况是:100 ~ 199,1100 ~ 1199,2100 ~ 2199,……,11100 ~ 11199,12100 ~ 12199,一共有1300个,并且等于更高位数字+1(12+1)乘以当前位数(100)。

又以判断位i=10为例:

  1. 对数字305,current = 0,high=3,low=5,则count=high*i;
  2. 对数字315,current = 1,high=3,low=5,则count=high*i+low+1;
  3. 对数字345,current > 1(即>=2),high=3,low=5,则count=(high+1)*i;
public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        int count = 0; // 计数
        int i = 1; // 当前的判断位
        int current = 0, high = 0, low = 0;
        while (n / i != 0) {
            high = n / (i * 10); // 高位
            current = n / i % 10; // 当前位
            low = n % i; // 低位
            if (current > 1) {
                count += (high + 1) * i;
            } else if (current == 1) {
                count += high * i + low + 1;
            } else if (current == 0) {
                count += high * i;
            }
            i *= 10;
        }
        return count;
    }
}

32、把数组排成最小的数

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

思路:

先将整型数组添加到ArrayList列表中,然后将列表排序,最后将排好序的字符串列表拼接出来。

关键就是制定排序规则。

排序规则如下:

  1. 若ab > ba 则 a > b,

  2. 若ab < ba 则 a < b,

  3. 若ab = ba 则 a = b;

解释说明:

比如 “3” < "31"但是 “331” > “313”,所以要将二者拼接起来进行比较

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
/*
 * 
 */
public class Solution {
    public String PrintMinNumber(int [] numbers) {
        ArrayList<Integer> list = new ArrayList<>();
        String str = "";
        for (int i = 0; i < numbers.length; i++) {
            list.add(numbers[i]);
        }
        // 按排序规则排序
        Collections.sort(list, new Comparator<Integer>() {
            @Override
            public int compare(Integer n1, Integer n2) {
                String s1 = n1 + "" + n2;
                String s2 = n2 + "" + n1;
                return s1.compareTo(s2);
            }
        });
        for (int i = 0; i < list.size(); i++) {
            str += list.get(i);
        }
        return str;
    }
}
33、求第N个丑数

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

思路:

首先从丑数的定义我们知道,一个丑数的因子只有2、3、5,那么丑数 p = 2 x ∗ 3 y ∗ 5 z p=2^x*3^y*5^z p=2x3y5z ,换句话说一个丑数一定由另一个丑数乘以2或者乘以3或者乘以5得到,那么我们从1开始乘以2、3、5,就得到2、3、5三个丑数,在从这三个丑数出发乘以2、3、5就得到4、6、10、6、9、15、10、15、25九个丑数,我们发现这种方法会得到重复的丑数,而且我们题目要求第N个丑数,这样的方法得到的丑数也是无序的。那么我们可以维护三个队列:

(1)丑数数组:1

乘以2的队列:2

乘以3的队列:3

乘以5的队列:5

选择三个队列头最小的数2加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

(2)丑数数组:1,2

​ 乘以2的队列:4

​ 乘以3的队列:3,6

​ 乘以5的队列:5,10

​ 选择三个队列头最小的数3加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

(3)丑数数组:1,2,3

​ 乘以2的队列:4,6

​ 乘以3的队列:6,9

​ 乘以5的队列:5,10,15

​ 选择三个队列头里最小的数4加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

(4)丑数数组:1,2,3,4

​ 乘以2的队列:6,8

​ 乘以3的队列:6,9,12

​ 乘以5的队列:5,10,15,20

​ 选择三个队列头里最小的数5加入丑数数组,同时将该最小的数乘以2,3,5放入三个队列;

(5)丑数数组:1,2,3,4,5

​ 乘以2的队列:6,8,10

​ 乘以3的队列:6,9,12,15

​ 乘以5的队列:10,15,20,25

​ 选择三个队列头里最小的数6加入丑数数组,但我们发现,有两个队列头都为6,所以我们弹出两个队列头,同时将12,18,30放入三个队列;

​ ……………………

疑问:

1.为什么分三个队列?

​ 丑数数组里的数一定是有序的,因为我们是从丑数数组里的数乘以2,3,5选出的最小数,一定比以前未乘以2,3,5大,同时对于三个队列内部,按先后顺序乘以2,3,5分别放入,所以同一个队列内部也是有序的;

2.为什么比较三个队列头部最小的数放入丑数数组?

​ 因为三个队列是有序的,所以取出三个头中最小的,等同于找到了三个队列所有数中最小的。

实现思路:

​ 我们没有必要维护三个队列,只需要记录三个指针显示到达哪一步;“|”表示指针,arr表示丑数数组;

​ (1)1

​ |2

​ |3

​ |5

​ 目前指针指向0,0,0,队列头arr[0] * 2 = 2, arr[0] * 3 = 3, arr[0] * 5 = 5

​ (2)1 2

​ 2 |4

​ |3 6

​ |5 10

​ 目前指针指向1,0,0,队列头arr[1] * 2 = 4, arr[0] * 3 = 3, arr[0] * 5 = 5

​ (3)1 2 3

​ 2 |4 6

​ 3 |6 9

​ |5 10 15

目前指针指向1,1,0,队列头arr[1] * 2 = 4, arr[1] * 3 = 6, arr[0] * 5 = 5

………………

public class Solution {
    public int GetUglyNumber_Solution(int index) {
        // 0~6的丑数为0~6
        if (index < 7) {
            return index;
        }
        int[] arr = new int[index];
        arr[0] = 1;
        int p2 = 0;
        int p3 = 0;
        int p5 = 0;
        for (int i = 1; i < index; i++) {
            arr[i] = Math.min(Math.min(arr[p2] * 2, arr[p3] * 3), arr[p5] * 5);
            if (arr[p2] * 2 == arr[i]) {
                p2++;
            }
            if (arr[p3] * 3 == arr[i]) {
                p3++;
            }
            if (arr[p5] * 5 == arr[i]) {
                p5++;
            }
        }
        return arr[index - 1];
    }
}
34、第一个只出现一次的字符

在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).

思路:

使用LinkedHashMap保存出现次数。

LinkedHashMap的保存顺序与输入顺序一致。

import java.util.LinkedHashMap;
 
public class Solution {
    public int FirstNotRepeatingChar(String str) {
        LinkedHashMap<Character, Integer> map = new LinkedHashMap<>();
        for (int i = 0; i < str.length(); i++) {
            char key = str.charAt(i);
            if (map.containsKey(key)) {
                map.put(key, map.get(key) + 1);
            } else {
                map.put(key, 1);
            }
        }
 
        for (int i = 0; i < str.length(); i++) {
            char key = str.charAt(i);
            if (map.get(key) == 1) {
                return i;
            }
        }
        return -1;
    }
}
35、数组中的逆序对

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

输入描述:

题目保证输入的数组中没有的相同的数字
数据范围:	
	对于%50的数据,size<=10^4	
	对于%75的数据,size<=10^5	
	对于%100的数据,size<=2*10^5

输入:

1,2,3,4,5,6,7,0

输出:

7

思路:

本质是归并排序。

把数据分成前后两个数组(递归分到每个数组仅有一个数据项)后,合并数组。其中,前一个数组长度为[start, mid],后一个数组的长度为[mid+1, end]。

合并时,出现前面的数组值arr[i]大于后面数组值arr[j]时,即说明前面数组arr[i]~arr[mid]都是大于arr[j]的,此时count += mid-i+1。

public class Solution {
    int count = 0;
 
    public int InversePairs(int[] array) {
        if (array.length < 2) {
            return 0;
        }
        int[] tmp = new int[array.length];
        mergeSort(array, tmp, 0, array.length - 1);
        return count % 1000000007;
    }
 
    public void mergeSort(int[] arr, int[] tmp, int start, int end) {
        if (start < end) {
            int mid = (start + end) / 2; // mid指向左序列的最后一位
            mergeSort(arr, tmp, start, mid);
            mergeSort(arr, tmp, mid + 1, end);
            merge(arr, tmp, start, mid, end);
        }
    }
 
    public void merge(int[] arr, int[] tmp, int start, int mid, int end) {
        int index = start, left = start, right = mid + 1; // tmp索引,left索引,right索引
        while (left <= mid && right <= end) {
            if (arr[left] <= arr[right]) {
                tmp[index++] = arr[left++];
            } else {
                tmp[index++] = arr[right++];
                count += mid - left + 1; // left数组中,比arr[left]还大的数还有mid-i个,这些数字都比arr[right]大,所以要加mid-left+1
                count %= 1000000007;
            }
        }
        while (left <= mid) {
            tmp[index++] = arr[left++];
        }
        while (right <= end) {
            tmp[index++] = arr[right++];
        }
        for (int i = start; i <= end; i++) {
            arr[i] = tmp[i];
        }
    }
}
36、两个链表的第一个公共节点

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

思路:

找出2个链表的长度,然后让长的先走两个链表的长度差,然后再一起走

(因为2个链表用公共的尾部)

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

    ListNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        int len1 = getLength(pHead1);
        int len2 = getLength(pHead2);
        if (len1 > len2) {
            for (int i = 0; i < len1 - len2; i++) {
                pHead1 = pHead1.next;
            }
        } else {
            for (int i = 0; i < len2 - len1; i++) {
                pHead2 = pHead2.next;
            }
        }
        while (pHead1 != pHead2) {
            pHead1 = pHead1.next;
            pHead2 = pHead2.next;
        }
        return pHead1;
    }
 
    public int getLength(ListNode head) {
        int len = 0;
        ListNode root = head;
        while (root != null) {
            root = root.next;
            len++;
        }
        return len;
    }
}
37、数字在排序数组中出现的次数

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

思路:

利用二分查找+递归思想,进行寻找。当目标值与中间值相等时进行判断

public class Solution {
    public int GetNumberOfK(int[] array, int k) {
        int start = 0, end = array.length - 1;
        int first = getFirst(array, start, end, k);
        int last = getLast(array, start, end, k);
        if (first == -1 && last == -1) {
            return 0;
        } else {
            return last - first + 1;
        }
    }
 
    // 递归写法
    public int getFirst(int[] arr, int start, int end, int k) {
        while (start <= end) {
            int mid = (start + end) / 2;
            if (arr[mid] < k) {
                return getFirst(arr, mid + 1, end, k);
            } else if (arr[mid] > k) {
                return getFirst(arr, start, mid - 1, k);
            } else { // arr[mid] == k
                if ((mid > 0 && arr[mid - 1] != k) || mid == 0) { // 找到了
                    return mid;
                } else { // mid > 0 && arr[mid + 1] == k,即第一个k在前半截
                    return getFirst(arr, start, mid - 1, k);
                }
            }
        }
        return -1;
    }
 
    // 循环写法
    public int getLast(int[] arr, int start, int end, int k) {
        while (start <= end) {
            int mid = (start + end) / 2;
            if (arr[mid] < k) {
                start = mid + 1;
            } else if (arr[mid] > k) {
                end = mid - 1;
            } else { // arr[mid] == k
                if ((mid < end && arr[mid + 1] != k) || mid == end) { // 找到了
                    return mid;
                } else { // mid < end && arr[mid + 1] == k,即最后一个k在后半截
                    start = mid + 1;
                }
            }
        }
        return -1;
    }
}
38、二叉树的深度

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

思路:

  1. 递归:深度遍历
/**
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public int TreeDepth(TreeNode root) {
        if (root != null) {
            return 1 + Math.max(TreeDepth(root.left), TreeDepth(root.right));
        } else {
            return 0;
        }
    }
}
  1. 非递归:层次遍历的思想
import java.util.LinkedList;
import java.util.Queue;
 
public class Solution {
    public int TreeDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        Queue<TreeNode> que = new LinkedList<>();
        que.add(root);
        int depth = 0;
        int count = 0; // 当前层已遍历的节点个数
        int nextCount = 1; // 下一层有几个节点(第一层有一个节点)
        while (!que.isEmpty()) {
            TreeNode node = que.poll();
            count++;
            if (node.left != null) {
                que.add(node.left);
            }
            if (node.right != null) {
                que.add(node.right);
            }
            if (count == nextCount) {
                nextCount = que.size(); // 置换为下一层的节点数
                count = 0; // 重新计数
                depth++;
            }
        }
        return depth;
    }
}
39、平衡二叉树

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

思路:

自下而上的方法。

若左右子树不平衡,则返回-1,退出遍历。若左右子树平衡,则返回当前子树的高度。

public class Solution {
    // 自下而上
    public boolean IsBalanced_Solution(TreeNode root) {
        return getBalancedDepth(root) != -1;
    }
 
    // 若左右子树不平衡,返回-1
    // 若左右子树平衡,返回当前子树的高度
    private int getBalancedDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int left = getBalancedDepth(root.left);
        if (left == -1) {
            return -1;
        }
        int right = getBalancedDepth(root.right);
        if (right == -1) {
            return -1;
        }
        return Math.abs(left - right) > 1 ? -1 : 1 + Math.max(left, right);
    }
}
40、数组中只出现一次的数字

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

思路:

首先:位运算中异或的性质:两个相同数字异或=0,一个数和0异或还是它本身

只有一个数出现一次时,我们把数组中所有的数,依次异或运算,最后剩下的就是落单的数,因为成对儿出现的都抵消了。

依照这个思路,我们来看两个数(我们假设是AB)出现一次的数组。我们首先还是先异或,剩下的数字肯定是A、B异或的结果,这个结果的二进制中的1,表现的是A和B的不同的位。我们就取第一个1所在的位数,假设是第3位,接着把原数组分成两组,分组标准是第3位是否为1。如此,相同的数肯定在一个组,因为相同数字所有位都相同,而不同的数,肯定不在一组。然后把这两个组按照最开始的思路,依次异或,剩余的两个结果就是这两个只出现一次的数字。

//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public class Solution {
    public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        if (array == null || array.length <= 1) {
            num1[0] = num2[0] = 0;
            return;
        }
        int sum = 0;
        for (int i = 0; i < array.length; i++) {
            sum ^= array[i];
        }
        
        // 找到最右边的1在哪一位
        int index = 0; // 1所在的位置
        while (((sum & 1) == 0) && index < 32){ // int类型4字节,32位
            sum >>= 1;
            index++;
        }
        
        for (int i = 0; i < array.length; i++) {
            if (((array[i] >> index) & 1) == 1) {
                num1[0] ^= array[i];
            } else {
                num2[0] ^= array[i];
            }
        }
    }
}
41、和为S的连续正数序列

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!

输出描述:

输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序

思路:

双指针问题,相当于一个窗口,窗口的左右两边是两个指针small和big。

当small和big之间的序列和小于sum时,big++;大于sum时,small++。

当small增加到(1+sum)/2时停止。

也可实现给定数列中,选出总和为sum的连续数字。

import java.util.ArrayList;

public class Solution {
    public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> result = new ArrayList<>();
        int small = 1, big = 2, middle = (sum + 1) / 2;
        int s = small + big;
        while (small < big && small <= middle) {
            if (s == sum) {
                ArrayList<Integer> list = new ArrayList<>();
                for (int i = small; i <= big; i++) {
                    list.add(i);
                }
                result.add(list);
                big++;
                s += big;
            } else if (s < sum) { // s<sum时,需要先big++,再加上新的数字
                big++;
                s += big;
            } else { // s>sum时,需要减去当前的最小值,再small++
                s -= small;
                small++;
            }
        }
        return result;
    }
}
42、和为S的两个数字

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

输出描述:

对应每个测试案例,输出两个数,小的先输出。

思路:

输出两个数的乘积最小的。对这句话的理解?

**假设:**若存在b>a满足要求,则存在 a + b = s a+b=s a+b=s ( a − m ) + ( b + m ) = s (a-m)+(b+m)=s (am)+(b+m)=s

( a − m ) ( b + m ) = a b − ( b − a ) m − m ∗ m &lt; a b (a - m )(b + m)=ab - (b-a)m - m*m &lt; ab (am)(b+m)=ab(ba)mmm<ab说明外层(即 a − m a-m am b + m b+m b+m)的乘积更小

也就是说依然是左右夹逼法!!!只需要2个指针

  1. small开头**,**big指向结尾
  2. 如果和小于sum,说明太小了small右移寻找更的数
  3. 如果和大于sum,说明太大了big左移寻找更的数
  4. 和相等,把small和big的数返回
import java.util.ArrayList;

public class Solution {
    public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
        ArrayList<Integer> list = new ArrayList<>();
        int small = 0, big = array.length - 1;
        while (small < big) {
            int s = array[small] + array[big];
            if (s == sum) {
                list.add(array[small]);
                list.add(array[big]);
                break;
            } else if (s > sum) {
                big--;
            } else { // s<sum
                small++;
            }
        }
        return list;
    }
}
43、左旋转字符串

汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!

思路:

原理:YX = (XTYT)T

需要注意:当数组长度小于要求长度时,要返回空字符串""

public class Solution {
    public String LeftRotateString(String str, int n) {
        if (str.length() < n) {
            return "";
        }
        StringBuffer sb = new StringBuffer();
        char[] ch = str.toCharArray();
        reverse(ch, 0, n - 1);
        reverse(ch, n, ch.length - 1);
        reverse(ch, 0, ch.length - 1);
        sb.append(ch);
        return sb.toString();
    }
 
    public static void reverse(char[] ch, int start, int end) {
        while (start < end) {
            char tem = ch[start];
            ch[start] = ch[end];
            ch[end] = tem;
            start++;
            end--;
        }
    }
}
44、反转单词顺序列

牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?

思路:

先将整个字符串全部反转,再以’ '为界线依次反转每个单词。

注意:最后一个单词没有’ ',需要单独进行反转。

public class Solution {
    public String ReverseSentence(String str) {
        char[] ch = str.toCharArray();
        reverse(ch, 0, ch.length - 1);
        int start = 0, end = 0;
        for (int i = 0; i < ch.length; i++) {
            if (ch[i] == ' ') {
                end = i - 1;
                reverse(ch, start, end);
                start = i + 1;
            }
        }
        reverse(ch, start, ch.length - 1); // 最后一个单词后没有' ',需要单独进行反转
        StringBuffer sb = new StringBuffer();
        sb.append(ch);
        return sb.toString();
    }
 
    public static void reverse(char[] ch, int start, int end) {
        while (start < end) {
            char tem = ch[start];
            ch[start] = ch[end];
            ch[end] = tem;
            start++;
            end--;
        }
    }
}
45、扑克牌顺子

LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张)…他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子…LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。

思路:

  1. 数组从小到大排序;

  2. 计算数组中0的个数;

  3. 计算数组中空缺个数;

    同时需要判断是否有两个一样的数,如果有则一定不成功。

  4. 若0的个数>=空缺个数,为true(当四个王,一个数字时,0的个数为4,空缺个数为0);否则为false。

import java.util.Arrays;
 
public class Solution {
    public boolean isContinuous(int [] numbers) {
        if (numbers.length == 0) {
            return false;
        }
        Arrays.sort(numbers); // 数组从小到大排序
        int numOfZero = 0;
        int numOfGap = 0;
        for (int i = 0; i < numbers.length - 1; i++) {
            if (numbers[i] == 0) { // 计算数组中0的个数
                numOfZero++;
                continue;
            }
            if (numbers[i] == numbers[i + 1]) { // 判断是否有两个一样的数,如果有则一定不成功
                return false;
            }
            numOfGap += numbers[i + 1] - numbers[i] - 1; // 计算数组中空缺个数
        }
        if (numOfZero >= numOfGap) {
            return true;
        } else {
            return false;
        }
    }
}
46、孩子们的游戏(圆圈中最后剩下的数)

每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0…m-1报数…这样下去…直到剩下最后一个小朋友,可以不用表演,,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)

如果没有小朋友,请返回-1

思路:

约瑟夫环问题

经典解法:采用链表方式模拟圆圈

import java.util.LinkedList;
 
public class Solution {
    public int LastRemaining_Solution(int n, int m) {
        if (n < 1 || m < 1) {
            return -1;
        }
        LinkedList<Integer> list = new LinkedList<>();
        for (int i = 0; i < n; i++) {
            list.add(i);
        }
        int num = 0;
        while (list.size() > 1) {
            num = (num + m - 1) % list.size();
            list.remove(num);
        }
        return list.get(0);
    }
}
47、求1+2+3+…+n

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

思路:

通过逻辑与(&&)的短路特性实现递归终止

public class Solution {
    public int Sum_Solution(int n) {
        int sum = n;
        boolean flag = (n > 0) && ((sum += Sum_Solution(n - 1)) > 0);
        return sum;
    }
}
48、不用加减乘除做加法

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

思路:

加法的步骤:(以5+17=22为例,5=101(2),17=10001(2)

  1. 各位相加但不计进位(得到结果10100)
  2. 记下进位(只在最后一位相加时产生一个进位,结果为10)
  3. 把前两步的结果相加(得到结果10110)

对二进制数进行相同处理,能够发现:

  1. 各位相加,不考虑进位(0加0、1加1的结果都为0;0加1、1加0的结果都为1):异或运算

  2. 求进位(0加0、0加1、1加0都不会产生进位;只有1加1会产生进位):两个数先做位与运算,再向左移动一位

  3. 将上述两步的结果相加。相加的方式仍然是重复步骤1、2,直到不产生进位为止。

public class Solution {
    public int Add(int num1,int num2) {
        while (num2 != 0) { 
            int sum = num1 ^ num2;
            num2 = (num1 & num2) << 1; // num2用来存储进位值(carry)
            num1 = sum; // num1用来存储各位相加,不考虑进位的值
        }
        return num1;
    }
}
49、把字符串转换成整数

将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。

输入描述:

输入一个字符串,包括数字字母符号,可以为空

输出描述:

如果是合法的数值表达则返回该数字,否则返回0

示例1

输入

+2147483647
    1a33

输出

2147483647
    0

思路:

需要注意的边界条件:

  1. 是否为null或全空格的字符串?
  2. 只有“+”或“-”?
  3. 存在不为0~9的字符?
  4. 数据是否溢出?

还要注意输入的是正数还是负数。

public class Solution {
    public int StrToInt(String str) {
        boolean isLegal = true; // 判断输入是否合法
        if (str == null || str.trim().equals("")) { // str.tirm() 返回字符串的副本,忽略前导空白和尾部空白。
            isLegal = false;
            return 0;
        }
        int symbol = 0; // 符号位。若为0,则为正数;若为1,则为负数。
        int start = 0; // 判断是否有符号位
        char[] ch = str.trim().toCharArray();
        if (ch[0] == '+') {
            start = 1;
        }
        if (ch[0] == '-') {
            start = 1;
            symbol = 1;
        }
        if (start == ch.length) {
            isLegal = false;
            return 0;
        }
        int result = 0;
        for (int i = start; i < ch.length; i++) {
            if (ch[i] < '0' || ch[i] > '9') {
                isLegal = false;
                return 0;
            }
 
            // 该部分判断是否产生溢出
            int sum = result * 10 + ch[i] - '0';
            if ((sum - (ch[i] - '0')) / 10 != result) {
                isLegal = false;
                return 0;
            }
 
            result = sum; // result * 10 + ch[i] - '0'
        }
        return symbol == 0 ? result : result * -1;
    }
}
50、数组中重复的数字

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

思路:

最简单的就是用一个数组或者哈希表来存储已经遍历过的数字,但是这样需要开辟额外的空间。

如果题目要求不能开辟额外的空间,那我们可以用如下的方法:

  1. 从头到尾依次扫描这个数组中的每个数字。当扫描到下标为i的数字时,首先比较这个数字m是不是等于它的下标i。
  2. 如果是,接着扫描下一个数字。如果不是,再拿它和下标为m的数字进行比较,
  3. 如果它和第m个数字相等,则找到了一个重复的数字(该数字在下标为i和m的位置都出现了)。如果它和第m个数字不相等,就把第i个数字和第m个数字交换,把m放到属于它的位置。
  4. 不断重复这个过程,直到找到重复的数字。

false情况:

  1. 数组为空(非法)

  2. 数字不在0到n-1范围内(非法)

  3. 所有数字都不重复

public class Solution {
    // Parameters:
    //    numbers:     an array of integers
    //    length:      the length of array numbers
    //    duplication: (Output) the duplicated number in the array number,length of duplication array is 1,so using duplication[0] = ? in implementation;
    //                  Here duplication like pointor in C/C++, duplication[0] equal *duplication in C/C++
    //    这里要特别注意~返回任意重复的一个,赋值duplication[0]
    // Return value:       true if the input is valid, and there are some duplications in the array number
    //                     otherwise false
    public boolean duplicate(int numbers[],int length,int [] duplication) {
        if (numbers == null || length <= 0) { // 数组为空(非法)
            return false;
        }
        for (int i = 0; i < length; i++) { // 数字不在0到n-1范围内(非法)
            if (numbers[i] < 0 || numbers[i] > length - 1) {
                return false;
            }
        }
        for (int i = 0; i < length; i++) {
            while (numbers[i] != i) {
                int index = numbers[i];
                if (numbers[i] == numbers[index]) {
                    duplication[0] = numbers[i];
                    return true;
                } else {
                    int tem = numbers[i];
                    numbers[i] = numbers[index];
                    numbers[index] = tem;
                }
            }
        }
        return false; // 所有数字都不重复
    }
}
51、构建乘积数组

给定一个数组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]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1] B[i]=A[0]A[1]...A[i1]A[i+1]...A[n1]。不能使用除法。

思路:

定义 C [ i ] = A [ 0 ] ∗ A [ 1 ] ∗ . . . ∗ A [ i − 1 ] C[i]=A[0]*A[1]*...*A[i-1] C[i]=A[0]A[1]...A[i1] D [ i ] = A [ i + 1 ] ∗ . . . ∗ A [ n − 2 ] ∗ A [ n − 1 ] D[i]=A[i+1]*...*A[n-2]*A[n-1] D[i]=A[i+1]...A[n2]A[n1]。C[i]可以用自上而下的顺序计算出来,即 C [ i ] = C [ i − 1 ] ∗ A [ i − 1 ] C[i]=C[i-1]*A[i-1] C[i]=C[i1]A[i1]。类似的,D[i]可以用自下而上的顺序计算出来,即 D [ i ] = D [ i + 1 ] ∗ A [ i + 1 ] D[i]=D[i+1]*A[i+1] D[i]=D[i+1]A[i+1]

在这里插入图片描述

public class Solution {
    public int[] multiply(int[] A) {
        int[] B = new int[A.length];
        if (A.length != 0) {
            // 计算A[0]*A[1]*...*A[i-1]
            B[0] = 1;
            for (int i = 1; i < A.length; i++) {
                B[i] = B[i - 1] * A[i - 1];
            }
            // 计算A[i+1]*A[i+2]*...*A[n-2]*A[n-1],并归入B数组中
            int tem = 1;
            for (int i = A.length - 2; i >= 0; i--) {
                tem *= A[i + 1];
                B[i] *= tem;
            }
        }
        return B;
    }
}
52、正则表达式匹配

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

思路:

首先,考虑特殊情况:

  1. 两个字符串都为空,返回true
  2. 当第一个字符串不空,而第二个字符串空了,返回false(因为这样,就无法匹配成功了,而如果第一个字符串空了,第二个字符串非空,还是可能匹配成功的,比如第二个字符串是“aaaa”,由于‘*’之前的元素可以出现0次,所以有可能匹配成功)

之后就开始匹配第一个字符,这里有两种可能:匹配成功或匹配失败。但考虑到pattern下一个字符可能是‘*’, 这里我们分两种情况讨论:pattern下一个字符为‘*’或不为‘*’:

  1. pattern下一个字符不为‘*’:这种情况比较简单,直接匹配当前字符。如果匹配成功,继续匹配下一个;如果匹配失败,直接返回false。注意这里的“匹配成功”,除了两个字符相同的情况外,还有一种情况,就是pattern的当前字符为‘.’,同时str的当前字符不为‘\0’。

  2. pattern下一个字符为‘*’时,稍微复杂一些,因为‘*’可以代表0个或多个。

    这里把这些情况都考虑到:

    a. 当‘*’匹配0个字符时,str当前字符不变,pattern当前字符后移两位,跳过这个‘*’符号;

    b. 当‘*’匹配1个或多个时,str当前字符移向下一个,pattern当前字符不变。(这里匹配1个或多个可以看成一种情况,因为:当匹配一个时,由于str移到了下一个字符,而pattern字符不变,就回到了上边的情况a;当匹配多于一个字符时,相当于从str的下一个字符继续开始匹配)

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

public class Solution {
    public boolean match(char[] str, char[] pattern) {
        if (str == null || pattern == null) {
            return false;
        }
        int strIndex = 0, patternIndex = 0;
        return matchCore(str, strIndex, pattern, patternIndex);
    }
     
    public static boolean matchCore(char[] str, int strIndex, char[] pattern, int patternIndex) {
        // 两个字符串都为空,返回true
        if (strIndex == str.length && patternIndex == pattern.length) {
            return true;
        }
        // 当第一个字符串不空,而第二个字符串空了,返回false
        if (strIndex != str.length && patternIndex == pattern.length) {
            return false;
        }

        /*
         * 开始匹配第一个字符
         * 此时还有两种情况:(strIndex != str.length && patternIndex != pattern.length)
         *              及(strIndex == str.length && patternIndex != pattern.length)
         * 所以只需要判断strIndex是否越界
         */
        // 当pattern下一个字符为'*'时
        if (patternIndex + 1 != pattern.length && pattern[patternIndex + 1] == '*') {
            // '*'匹配一个或多个字符;当前字符为'.'
            if (strIndex != str.length && (str[strIndex] == pattern[patternIndex] || 
                                           pattern[patternIndex] == '.')) {
                	   // 当前字符相等的情况下,'*'匹配0个字符
                return matchCore(str, strIndex, pattern, patternIndex + 2) || 
                    matchCore(str, strIndex + 1, pattern, patternIndex); 
                	// '*'匹配1个字符(多个字符也可归类到其中)
            } else {
                // 当前字符不相等的情况下,'*'匹配0个字符
                return matchCore(str, strIndex, pattern, patternIndex + 2); 
            }
        } else { // pattern下一个字符不为'*'
            if (strIndex != str.length && (str[strIndex] == pattern[patternIndex] || 
                                           pattern[patternIndex] == '.')) {
                return matchCore(str, strIndex + 1, pattern, patternIndex + 1);
            } else {
                return false;
            }
        }
    }
}
53、表示数值的字符串

题目描述:

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。

例如,字符串"+100", “5e2”, “-123”, “3.1416” 和 "-1E-16"都表示数值。 但是 “12e”, “1a3.14”, “1.2.3”, “±5” 和 "12e+4.3"都不是。

方法一:

思路:

正则表达式解法:

以下对正则进行解释:
[\\+\\-]?            -> 正或负符号出现与否
\\d*                 -> 整数部分是否出现,如-.34+3.34均符合
(\\.\\d+)?           -> 如果出现小数点,那么小数点后面必须有数字;
                        否则一起不出现
([eE][\\+\\-]?\\d+)? -> 如果存在指数部分,那么e或E肯定出现,+-可以不出现,
						紧接着必须跟着整数;或者整个部分都不出现

代码

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

方法二

思路:

实现正则的判断思路

代码:

public class Solution {
    private int index = 0;

    public boolean isNumeric(char[] str) {
        if (str == null || str.length == 0) {
            return false;
        }

        boolean flag = scanInteger(str);
        if (index < str.length && str[index] == '.') {
            index++;
            flag = scanUnsignedInteger(str) || flag; // +12.e12 也符合要求
        }
        if (index < str.length && (str[index] == 'e' || str[index] == 'E')) {
            index++;
            flag = flag && scanInteger(str);
        }
        return flag && index == str.length;
    }

    private boolean scanInteger(char[] str) {
        if (index < str.length && (str[index] == '+' || str[index] == '-')) {
            index++;
        }
        return scanUnsignedInteger(str);
    }

    private boolean scanUnsignedInteger(char[] str) {
        int start = index;
        while (index < str.length && str[index] >= '0' && str[index] <= '9') {
            index++;
        }
        return start < index; // 判断是否有整数
    }
}
54、字符流中第一个不重复的字符

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

输出描述:

如果当前字符流没有存在出现一次的字符,返回#字符。

例:

输入:

“google”

对应输出应该为:

“ggg#ll”

思路:

设置一个HashMap记录不同字符出现次数,一个ArrayList记录目前出现过的字符

import java.util.ArrayList;
import java.util.HashMap;

public class Solution {
    HashMap<Character, Integer> map = new HashMap();
    ArrayList<Character> list = new ArrayList<>();

    //Insert one char from stringstream
    public void Insert(char ch) {
        if (map.containsKey(ch)) {
            map.put(ch, map.get(ch) + 1);
        } else {
            map.put(ch, 1);
        }
        list.add(ch);
    }

    //return the first appearence once char in current stringstream
    public char FirstAppearingOnce() {
        for (char c : list) {
            if (map.get(c) == 1) {
                return c;
            }
        }
        return '#';
    }
}
55、链表中环的入口结点

给一个链表,若其中包含环,请找出该链表的环的入口结点,否则,输出null。

思路:

判断链表中是否有环 ----- 有关单链表中环的问题

在这里插入图片描述

从上面的分析知道,当fast和slow相遇时,slow还没有走完链表,假设fast已经在环内循环了n(1<= n)圈。假设slow走了s步,则fast走了2s步,又由于 fast走过的步数 = s + n * r (s + 在环上多走的n圈),则有下面的等式:

(1) 2 ∗ s = s + n ∗ r 2 * s = s + n * r \tag{1} 2s=s+nr(1)

(2) = &gt; s = n ∗ r =&gt; s = n * r \tag{2} =>s=nr(2)

如果假设整个链表的长度是L,入口和相遇点的距离是x(如上图所示),起点到入口点的距离是a(如上图所示),则有:

(3) a + x = s = n ∗ r a + x = s = n * r \tag{3} a+x=s=nr(3)

(3)由(2)推出

(4) a + x = ( n − 1 ) ∗ r + r = ( n − 1 ) ∗ r + ( L − a ) a + x = (n - 1) * r + r = (n - 1) * r + (L - a) \tag{4} a+x=(n1)r+r=(n1)r+(La)(4)

(4)由环的长度 = 链表总长度 - 起点到入口点的距离求出

(5) a = ( n − 1 ) ∗ r + ( L − a − x ) a = (n - 1) * r + (L -a -x) \tag{5} a=(n1)r+(Lax)(5)

集合式子(5)以及上图我们可以看出,从链表起点head开始到入口点的距离a,与从slow和fast的相遇点(如图)到入口点的距离相等。

因此我们就可以分别用一个指针(ptr1, prt2),同时从head与slow和fast的相遇点出发,每一次操作走一步,直到ptr1 == ptr2,此时的位置也就是入口点!

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

    ListNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ListNode EntryNodeOfLoop(ListNode pHead) {
        ListNode slow = pHead;
        ListNode fast = pHead;
        while (slow != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                break;
            }
        }
        if (slow == null || fast.next == null) {
            return null;
        }
        slow = pHead;
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }
}
56、删除链表中重复的结点

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

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

    ListNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ListNode deleteDuplication(ListNode pHead) {
        ListNode root = new ListNode(-1);
        root.next = pHead;
        ListNode pPreNode = root; // 指向当前结点的上一个结点
        ListNode pNode = pHead; // 指向当前结点
        while (pNode != null && pNode.next != null) { // 不为空链表,不为尾结点
            if (pNode.val == pNode.next.val) {
                int val = pNode.val;
                while (pNode != null && pNode.val == val) {
                    pNode = pNode.next;
                }
                pPreNode.next = pNode;
            } else {
                pPreNode = pPreNode.next;
                pNode = pNode.next;
            }
        }
        return root.next;
    }
}
57、二叉树的下一个结点

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

思路:

首先知道中序遍历的规则是:左根右,然后作图
在这里插入图片描述
结合图,我们可发现分成两大类:

  1. 有右子树的,那么下个结点就是右子树最左边的点;(eg:D,B,E,A,C,G)

  2. 没有右子树的,也可以分成两类:

    a) 是父结点左孩子(eg:N,I,L) ,那么父结点就是下一个结点 ;

    b) 是父结点的右孩子(eg:H,J,K,M)找他的父结点的父结点的父结点……直到当前结点是其父结点的左孩子位置;

    c) 在b)的基础下,如果没有(eg:M),那么他就是尾结点。

/*
public class TreeLinkNode {
    int val;
    TreeLinkNode left = null;
    TreeLinkNode right = null;
    TreeLinkNode next = null;

    TreeLinkNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public TreeLinkNode GetNext(TreeLinkNode pNode) {
        if (pNode == null) {
            return null;
        }
        if (pNode.right != null) {
            pNode = pNode.right;
            while (pNode.left != null) {
                pNode = pNode.left;
            }
            return pNode;
        }
        while (pNode.next != null) {
            if (pNode == pNode.next.left) {
                return pNode.next;
            }
            pNode = pNode.next;
        }
        return null;
    }
}
58、对称的二叉树

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

思路:

根结点非空的情况下,满足以下三个条件,即为对称二叉树:

  1. 左孩子 与 右孩子 均为空 或 值相等 ;
  2. 左子树的左子树 与 右子树的右子树 相等;
  3. 左子树的右子树 与 右子树的左子树 相等。
/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

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

    }

}
*/
public class Solution {
    boolean isSymmetrical(TreeNode pRoot) {
        if (pRoot == null) {
            return true;
        }
        return helper(pRoot.left, pRoot.right);
    }

    boolean helper(TreeNode left, TreeNode right) {
        if (left == null && right == null) {
            return true;
        }
        if (left == null || right == null) {
            return false;
        }
        if (left.val != right.val) {
            return false;
        }
        return helper(left.left, right.right) && helper(left.right, right.left);
    }
}
59、按之字形顺序打印二叉树

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

思路:

需要两个栈。打印时,把下一层的子结点保存到相应的栈里。

  1. 若当前为奇数层,则先保存左子结点再保存右子结点到第一个栈里;
  2. 若当前为偶数层,则先保存右子结点再保存左子结点到第二个栈里。
import java.util.ArrayList;
import java.util.Stack;

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

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    public ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> result = new ArrayList<>();
        if (pRoot == null) {
            return result;
        }
        Stack<TreeNode> s1 = new Stack<>(); // 保存奇数层结点
        Stack<TreeNode> s2 = new Stack<>(); // 保存偶数层结点
        s1.push(pRoot);
        int depth = 1;
        while (!s1.empty() || !s2.empty()) {
            ArrayList<Integer> list = new ArrayList<>();
            if (depth % 2 != 0) { // 奇数层
                while (!s1.empty()) {
                    TreeNode node = s1.pop();
                    list.add(node.val);
                    if (node.left != null) {
                        s2.push(node.left);
                    }
                    if (node.right != null) {
                        s2.push(node.right);
                    }
                }
            } else { // 偶数层
                while (!s2.empty()) {
                    TreeNode node = s2.pop();
                    list.add(node.val);
                    if (node.right != null) {
                        s1.push(node.right);
                    }
                    if (node.left != null) {
                        s1.push(node.left);
                    }
                }
            }
            result.add(list);
            depth++;
        }
        return result;
    }
}
60、把二叉树打印成多行

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

思路:

设置一个任务队列list层次遍历所有节点;一个列表layer来记录当前层的所有结点;同时设置一个变量start作为计数器计算当前节点为该层的第几个节点,一个变量end记录该层一共有几个结点。

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

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

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    ArrayList<ArrayList<Integer>> Print(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> result = new ArrayList<>();
        if (pRoot == null) {
            return result;
        }
        ArrayList<Integer> layer = new ArrayList<>(); // 存储每一层的结点
        Queue<TreeNode> list = new LinkedList<>(); // 任务队列
        list.add(pRoot);
        int start = 0; // 每一层的计数器
        int end = 1; // 每一层结点总数
        while (!list.isEmpty()) {
            TreeNode pNode = list.poll();
            if (pNode.left != null) {
                list.add(pNode.left);
            }
            if (pNode.right != null) {
                list.add(pNode.right);
            }
            layer.add(pNode.val);
            start++;
            if (start == end) {
                result.add(layer);
                layer = new ArrayList<>();
                start = 0;
                end = list.size();
            }
        }
        return result;
    }
}
61、序列化二叉树

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

思路:

根据前序遍历规则完成序列化与反序列化。因为前序遍历序列是从根结点开始的。

所谓序列化指的是遍历二叉树为字符串;所谓反序列化指的是依据字符串重新构造成二叉树。

当在遍历二叉树时碰到Null指针时,这些Null指针被序列化为一个特殊的字符“#“。 另外,结点之间的数值用逗号隔开。

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

    public TreeNode(int val) {
        this.val = val;
    }
}
*/
public class Solution {
    String Serialize(TreeNode root) {
        StringBuffer sb = new StringBuffer();
        if (root == null) {
            sb.append("#");
            return sb.toString();
        }
        sb.append(root.val);
        sb.append("," + Serialize(root.left));
        sb.append("," + Serialize(root.right));
        return sb.toString();
    }

    int index = -1;

    TreeNode Deserialize(String str) {
        index++;
        String[] tree = str.split(",");
        if (tree == null) {
            return null;
        }
        TreeNode node = null;
        if (!tree[index].equals("#")) {
            node = new TreeNode(Integer.parseInt(tree[index]));
            node.left = Deserialize(str);
            node.right = Deserialize(str);
        }
        return node;
    }
}
62、二叉搜索树的第k个结点

给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。

思路:

中序遍历求第k个结点

  1. 非递归中序遍历
/*
public class TreeNode {
    int val = 0;
    TreeNode left = null;
    TreeNode right = null;

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

import java.util.Stack

public class Solution {
    TreeNode KthNode(TreeNode pRoot, int k) {
        if (pRoot == null || k == 0) {
            return null;
        }
        Stack<TreeNode> s = new Stack<>();
        int count = 0;
        TreeNode node = pRoot;
        while (node != null || !s.empty()) {
            if (node != null) {
                s.push(node);
                node = node.left;
            } else { // 到了当前所在子树最左边的节点
                node = s.pop();
                count++;
                if (count == k) {
                    return node;
                }
                node = node.right;
            }
        }
        return null; // k大于树的结点数
    }
}
  1. 递归中序遍历
public class Solution {
    int index = 0;

    TreeNode KthNode(TreeNode pRoot, int k) {
        if (pRoot != null) {
            TreeNode node = KthNode(pRoot.left, k); // 找到左子树中第k个结点
            if (node != null) { // 确认是否有左子树。若有,返回node
                return node;
            }
            index++; // 由于没有左子树,要加上当前结点的数(+1)
            if (index == k) { // 若计数器==k,找的就是当前点
                return pRoot;
            }
            // 找到右子树中第k个结点(计数器已经减掉之前的数了)
            node = KthNode(pRoot.right, k); 
            return node;
        }
        return null;
    }
}
63、数据流中的中位数

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

思路:

Java的PriorityQueue是从JDK1.5开始提供的新的数据结构接口,默认内部是自然排序,结果为小根堆,也可以自定义排序器,比如下面反转比较,完成大根堆。

为了保证插入新数据和取中位数的时间效率都高效,这里使用大根堆+小根堆的容器,并且满足:

  1. 两个堆中的数据数目差不能超过1(平均分配),这样可以使中位数只会出现在两个堆的交接处;

    **实现方法:**在数据的总数目是偶数时,把新数据插入到小根堆中,否则插入大根堆中。

  2. 大根堆的所有数据都小于小根堆,这样就满足了排序要求。

    **实现方法:**当数据的总数目是偶数时,先把新数据插入大根堆中,接着把大根堆中的最大数字拿出来插入小根堆中。这样就保证了小根堆中所有数字都大于大根堆。当数据总数目为奇数时处理方法相同。

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

public class Solution {
    int count = 0; // 计数器
    PriorityQueue<Integer> minHeap = new PriorityQueue<>();
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2.compareTo(o1);
        };
    });

    public void Insert(Integer num) {
        count++;
        if ((count & 1) == 0) { // 若为偶数个,插入小根堆
            if (!maxHeap.isEmpty() && num < maxHeap.peek()) {
                maxHeap.add(num);
                num = maxHeap.poll();
            }
            minHeap.add(num);
        } else { // 若为奇数个,插入大根堆(中位数)
            if (!minHeap.isEmpty() && num > minHeap.peek()) {
                minHeap.add(num);
                num = minHeap.poll();
            }
            maxHeap.add(num);
        }
    }

    public Double GetMedian() {
        double result = 0.0;
        if ((count & 1) == 0) {
            result = (minHeap.peek() + maxHeap.peek()) / 2.0;
        } else {
            result = maxHeap.peek();
        }
        return result;
    }
}
64、滑动窗口的最大值

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

思路:

滑动窗口应当是队列,但为了得到滑动窗口的最大值,队列序可以从两端删除元素,因此使用双端队列。

原则:对新来的元素k,将其与双端队列中的元素相比较

  1. 队列中比k小的数,直接移出队列(因为不再可能成为后面滑动窗口的最大值了!)

  2. 队列中比k大的数X,比较两者下标,判断X是否已不在窗口之内,不在了,直接移出队列

    为了能判断X是否已不再窗口内,需要在队列中保存数字的下标,而不是数值。当一个数字的下标与当前处理的数字的下标之差大于或等于窗口的大小时,该数字已从窗口中滑出。

    队列的第一个元素是滑动窗口中的最大值

    队列的第一个元素是滑动窗口中的最大值

队列的第一个元素是滑动窗口中的最大值

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

public class Solution {
    public ArrayList<Integer> maxInWindows(int[] num, int size) {
        ArrayList<Integer> result = new ArrayList<>();
        if (num == null || size < 1 || num.length < size) {
            return result;
        }
        Deque<Integer> indexDeque = new LinkedList<>();
        for (int i = 0; i < size - 1; i++) {
            while (!indexDeque.isEmpty() && num[i] > num[indexDeque.getLast()]) {
                indexDeque.removeLast();
            }
            indexDeque.addLast(i);
        }
        for (int i = size - 1; i < num.length; i++) {
            while (!indexDeque.isEmpty() && num[i] > num[indexDeque.getLast()]) {
                indexDeque.removeLast();
            }
            indexDeque.addLast(i);
            if ((i - indexDeque.getFirst() + 1) > size) { // 判断数字是否还在窗口中
                indexDeque.removeFirst();
            }
            result.add(num[indexDeque.getFirst()]);
        }
        return result;
    }
}
65、矩阵中的路径

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

[ a b c e s f c s a d e e ] \begin{bmatrix} a &amp; b &amp; c &amp; e \\ s &amp; f &amp; c &amp; s \\ a &amp; d &amp; e &amp; e \end{bmatrix} asabfdcceese

这样的3 X 4 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

思路:

回溯法

这是一个可以用回朔法解决的典型题。

  1. 首先,在矩阵中任选一个格子作为路径的起点。如果路径上的第i个字符不是ch,那么这个格子不可能处在路径上的第i个位置。如果路径上的第i个字符正好是ch,那么往相邻的格子寻找路径上的第i+1个字符。除在矩阵边界上的格子之外,其他格子都有4个相邻的格子。

  2. 重复这个过程直到路径上的所有字符都在矩阵中找到相应的位置。

  3. 由于回朔法的递归特性,路径可以被开成一个栈。当在矩阵中定位了路径中前n个字符的位置之后,在与第n个字符对应的格子的周围都没有找到第n+1个字符,这个时候只要在路径上回到第n-1个字符,重新定位第n个字符。

  4. 路径不能重复进入矩阵的格子,还需要定义和字符矩阵大小一样的布尔值矩阵,用来标识路径是否已经进入每个格子。 当矩阵中坐标为(row,col)的格子和路径字符串中相应的字符一样时,从4个相邻的格子(row,col-1),(row-1,col),(row,col+1)以及(row+1,col)中去定位路径字符串中下一个字符如果4个相邻的格子都没有匹配字符串中下一个的字符,表明当前路径字符串中字符在矩阵中的定位不正确,我们需要回到前一个,然后重新定位。

  5. 一直重复这个过程,直到路径字符串上所有字符都在矩阵中找到合适的位置

public class Solution {
    public boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
        boolean[] visited = new boolean[matrix.length];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (helper(matrix, rows, cols, i, j, str, 0, visited)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean helper(char[] matrix, int rows, int cols, int row, int col, 
                          char[] str, int strIndex, boolean[] visited) {
        if (strIndex == str.length) {
            return true;
        }
        int index = row * cols + col;
        if (row >= 0 && row < rows && col >= 0 && col < cols 
            && matrix[index] == str[strIndex] && !visited[index]) {
            visited[index] = true;
            if (helper(matrix, rows, cols, row - 1, col, str, strIndex + 1, visited) 
             || helper(matrix, rows, cols, row + 1, col, str, strIndex + 1, visited)
             || helper(matrix, rows, cols, row, col - 1, str, strIndex + 1, visited)
             || helper(matrix, rows, cols, row, col + 1, str, strIndex + 1, visited)) {
                return true;
            }
            visited[index] = false;
        }
        return false;
    }
}
66、机器人的运动范围

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

思路:

回溯法

类似第66题解法

public class Solution {
    public int movingCount(int threshold, int rows, int cols) {
        boolean[][] visited = new boolean[rows][cols];
        return helper(threshold, rows, cols, 0, 0, visited);
    }

    private int helper(int threshold, int rows, int cols, 
                       int row, int col, boolean[][] visited) {
        int count = 0;
        if (row >= 0 && row < rows && col >= 0 && col < cols 
            && !visited[row][col] && getSum(row) + getSum(col) <= threshold) {
            visited[row][col] = true;
            count = 1 + helper(threshold, rows, cols, row - 1, col, visited)
                    + helper(threshold, rows, cols, row + 1, col, visited)
                    + helper(threshold, rows, cols, row, col - 1, visited)
                    + helper(threshold, rows, cols, row, col + 1, visited);
        }
        return count;
    }

    private int getSum(int num) {
        int sum = 0;
        while (num != 0) {
            sum += num % 10;
            num /= 10;
        }
        return sum;
    }
}
67、剪绳子

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

输入描述:

输入一个数n,意义见题面。(2 <= n <= 60)

输出描述:

输出答案。

示例1

输入

8

输出

18

思路:

  1. 动态规划

动态规划求解问题的四个特征:

①求一个问题的最优解;

②整体的问题的最优解是依赖于各个子问题的最优解;

③小问题之间还有相互重叠的更小的子问题;

④从上往下分析问题,从下往上求解问题;

长度为i可得的最大乘积:dp[i] = dp[j]*dp[i-j]的最大值

public class Solution {
    public int cutRope(int target) {
        //动态规划:长度为i可得的最大乘积:dp[i]=dp[j]*dp[i-j]的最大值
        if (target < 2) {
            return 0; //可以不写这行
        }
        if (target == 2) {
            return 1;
        }
        if (target == 3) {
            return 2;
        }
        int[] dp = new int[target + 1];
        //边界值
        dp[0] = 0;
        dp[1] = 0;
        dp[2] = 2;
        dp[3] = 3;

        for (int i = 4; i <= target; i++) {
            int max = 0;
            for (int j = 1; j <= i / 2; j++) {
                max = Math.max(max, dp[j] * dp[i - j]);
            }
            dp[i] = max;
        }
        return dp[target];
    }
}
  1. 贪心算法

当n大于等于5时,我们尽可能多的剪长度为3的绳子;当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子。

为什么选2,3为最小的子问题?因为2,3包含于各个问题中,如果再往下剪得话,乘积就会变小。

为什么选长度为3?因为当n≥5时,3(n−3)≥2(n−2)

public class Solution {
    public int cutRope(int target) {
        // 贪心法,尽量剪出长度3,若最后剩长度4,就剪2*2
        if (target < 2) {
            return 0; // 可以不写这行
        }
        if (target == 2) {
            return 1;
        }
        if (target == 3) {
            return 2;
        }

        // 可以剪出的3的次数
        int timeOf3 = target / 3;

        // 判断最后剩下的一段长度是不是4(可能为4或5)
        if (target % 3 == 1) {
            timeOf3--; // 若为4,需要剪成2*2
        }

        // 剪完3后,剩下的剪2:可以剪出的2的次数
        int timeOf2 = (target - timeOf3 * 3) / 2;

        // 计算最终乘积(pow返回double型,注意类型转换)
        return (int) Math.pow(3, timeOf3) * (int) Math.pow(2, timeOf2);
    }
}
         || helper(matrix, rows, cols, row, col + 1, str, strIndex + 1, visited)) {
            return true;
        }
        visited[index] = false;
    }
    return false;
}

}


##### 66、机器人的运动范围

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

**思路:**

==回溯法==

类似第66题解法

```java
public class Solution {
    public int movingCount(int threshold, int rows, int cols) {
        boolean[][] visited = new boolean[rows][cols];
        return helper(threshold, rows, cols, 0, 0, visited);
    }

    private int helper(int threshold, int rows, int cols, 
                       int row, int col, boolean[][] visited) {
        int count = 0;
        if (row >= 0 && row < rows && col >= 0 && col < cols 
            && !visited[row][col] && getSum(row) + getSum(col) <= threshold) {
            visited[row][col] = true;
            count = 1 + helper(threshold, rows, cols, row - 1, col, visited)
                    + helper(threshold, rows, cols, row + 1, col, visited)
                    + helper(threshold, rows, cols, row, col - 1, visited)
                    + helper(threshold, rows, cols, row, col + 1, visited);
        }
        return count;
    }

    private int getSum(int num) {
        int sum = 0;
        while (num != 0) {
            sum += num % 10;
            num /= 10;
        }
        return sum;
    }
}
67、剪绳子

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

输入描述:

输入一个数n,意义见题面。(2 <= n <= 60)

输出描述:

输出答案。

示例1

输入

8

输出

18

思路:

  1. 动态规划

动态规划求解问题的四个特征:

①求一个问题的最优解;

②整体的问题的最优解是依赖于各个子问题的最优解;

③小问题之间还有相互重叠的更小的子问题;

④从上往下分析问题,从下往上求解问题;

长度为i可得的最大乘积:dp[i] = dp[j]*dp[i-j]的最大值

public class Solution {
    public int cutRope(int target) {
        //动态规划:长度为i可得的最大乘积:dp[i]=dp[j]*dp[i-j]的最大值
        if (target < 2) {
            return 0; //可以不写这行
        }
        if (target == 2) {
            return 1;
        }
        if (target == 3) {
            return 2;
        }
        int[] dp = new int[target + 1];
        //边界值
        dp[0] = 0;
        dp[1] = 0;
        dp[2] = 2;
        dp[3] = 3;

        for (int i = 4; i <= target; i++) {
            int max = 0;
            for (int j = 1; j <= i / 2; j++) {
                max = Math.max(max, dp[j] * dp[i - j]);
            }
            dp[i] = max;
        }
        return dp[target];
    }
}
  1. 贪心算法

当n大于等于5时,我们尽可能多的剪长度为3的绳子;当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子。

为什么选2,3为最小的子问题?因为2,3包含于各个问题中,如果再往下剪得话,乘积就会变小。

为什么选长度为3?因为当n≥5时,3(n−3)≥2(n−2)

public class Solution {
    public int cutRope(int target) {
        // 贪心法,尽量剪出长度3,若最后剩长度4,就剪2*2
        if (target < 2) {
            return 0; // 可以不写这行
        }
        if (target == 2) {
            return 1;
        }
        if (target == 3) {
            return 2;
        }

        // 可以剪出的3的次数
        int timeOf3 = target / 3;

        // 判断最后剩下的一段长度是不是4(可能为4或5)
        if (target % 3 == 1) {
            timeOf3--; // 若为4,需要剪成2*2
        }

        // 剪完3后,剩下的剪2:可以剪出的2的次数
        int timeOf2 = (target - timeOf3 * 3) / 2;

        // 计算最终乘积(pow返回double型,注意类型转换)
        return (int) Math.pow(3, timeOf3) * (int) Math.pow(2, timeOf2);
    }
}
参考链接
  1. 牛客网
  2. 【剑指offer】Java版代码(完整版)
  3. [剑指offer] JAVA版题解(全)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值