剑指offer部分解

文章目录

03_数组中重复的数字

题目描述

  • 找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

输入

[2, 3, 1, 0, 2, 5, 3]

输出

23

限制

2 <= n <= 100000

A方案

解决这个问题的一个简单的方法是先把输入的数组排序。从排序的数组中找出重复的数字是一件很容易的事情,只需要从头到尾扫描排序后的数组就可以了。排序一个长度为n的数组需要O(nlogn)的时间。

class Solution {
    //第一种方式,直接先排序再找出重复数字
    public int findRepeatNumber(int[] arr) {
        boolean flag;
        int temp;
        //冒泡排序
        for (int i = 1; i < arr.length; i++) {
            //设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。
            flag = true;
            for (int j = 0; j < arr.length - i; j++) {
                if (arr[j] > arr[j + 1]) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    flag = false;
                }
            }
            if (flag) {
                break;
            }
        }
        //遍历,如果扫描到的数字与前一个数字相等,则找到,否则,没有找到
        for (int i = 0; i < arr.length - 1; i++) {
            if (arr[i] == arr[i + 1]) {
                return arr[i];
            }
        }
        return -1;
    }
}

image-20210302155937357

B方案

还可以利用哈希表来解决这个问题。从头到尾按顺序扫描数组的每个数字,每扫描到一个数字的时候,都可以用O(1)的时间来判断哈希表里是否已经包含了该数字。如果哈希表里还没有这个数字,就把它加入哈希表。如果哈希表里已经存在该数字,就找到一个重复的数字。这个算法的时间复杂度是O(n),但它提高时间效率是以一个大小为O(n)的哈希表为代价的。

class Solution {
    public int findRepeatNumber(int[] arr) {
        int[] hashArr = new int[arr.length];
        //默认全0,如果不把第一位置-1,当遇到第一个0的时候就会被认为重复了
        hashArr[0] = -1;
        //hashArr数组循环下标由arr数组中的数所决定
        for (int j : arr) {
            //相等则表示遇到重复数字
            if (hashArr[j] == j) {
                return j;
            } else {
                //开始都为0,如果不相等则
                hashArr[j] = j;
            }
        }
        return -1;
    }
}

C方案

我们注意到数组中的数字都在0~n-1的范围内。如果这个数组中没有重复的数字,那么当数组排序之后数字i将出现在下标为i的位置。由于数组中有重复的数字,有些位置可能存在多个数字,同时有些位置可能没有数字。

现在让我们重排这个数组。从头到尾依次扫描这个数组中的每个数字。当扫描到下标为i的数字时,首先比较这个数字(用m表示)是不是等于i。如果是,则接着扫描下一个数字;如果不是,则再拿它和第m个数字进行比较。如果它和第m个数字相等,就找到了一个重复的数字(该数字在下标为im的位置都出现了);如果它和第m个数字不相等,就把第i个数字和第m个数字交换,把m放到属于它的位置。接下来再重复这个比较、交换的过程,直到我们发现一个重复的数字。

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

class Solution {
    public int findRepeatNumber(int[] arr) {
        int temp;
        /**举个例子:
         2,3,1,0,2,5,3
         */
        for (int i = 0; i < arr.length; i++) {
            /**
             * 第一遍进入的时候:2 != arr[2],也就是2 != 1应该让2去到他该去的地方
             * 第二遍进入,交换过来的arr[0]变成了1,再次判断 1 != arr[1],也就是1 != 3,又需要让他到arr[1]去,arr[1]=3,不相等,再次交换
             * 第三遍进入,交换过来的arr[0]变成了3,再次判断3 != arr[3],也就是3 != 0,再次执行交换
             * 第四遍进入,交换过来的arr[0]变成了0,再次判断0 == arr[0],跳出while循环,开始判断arr[1]。
             * ......
             */
            while (arr[i] != i) {
                /**
                 * 不能让他直接去,要判断一下原来的为止是否已经有一个相同的数字存在了,如果存在,则表示找到一个重复数字
                 */
                if (arr[i] == arr[arr[i]]) {
                    return arr[i];
                }
                /**
                 * 如果不相等,则交换,重新进入while循环
                 */
                temp = arr[i];
                arr[i] = arr[temp];
                arr[temp] = temp;
            }
        }
        return -1;
    }
}

题目延伸

  • 不修改数组找出重复的数字

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

A方案

这一题看起来和上一题类似。由于题目要求不能修改输入的数组,我们可以创建一个长度为n+1的辅助数组,然后逐一把原数组的每个数字复制到辅助数组。如果原数组中被复制的数字是m,则把它复制到辅助数组中下标为m的位置。这样很容易就能发现哪个数字是重复的。由于需要创建一个数组,该方案需要O(n)的辅助空间。

static class Solution {
    //第一种方式,直接先排序再找出重复数字
    public int findRepeatNumber(int[] arr) {
        int[] brr = new int[arr.length];
        for (int i : arr) {
            if (brr[i] != i) {
                brr[i] = i;
            } else {
                return i;
            }
        }
        return -1;
    }
}

B方案

接下来我们尝试避免使用O(n)的辅助空间。为什么数组中会有重复的数字?假如没有重复的数字,那么在从1~n的范围里只有n个数字。由于数组里包含超过n个数字,所以一定包含了重复的数字。看起来在某范围里数字的个数对解决这个问题很重要。

我们把从1~n的数字从中间的数字m分为两部分,前面一半为1~m,后面一半为m+1~n。如果1~m的数字的数目超过m,那么这一半的区间里一定包含重复的数字;否则,另一半m+1~n的区间里一定包含重复的数字。我们可以继续把包含重复数字的区间一分为二,直到找到一个重复的数字。这个过程和二分查找算法很类似,只是多了一步统计区间里数字的数目。

static class Solution {
    //第二种方式,二分
    public int findRepeatNumber(int[] arr) {
        //数字在1~n+1,所以起始数字不应该是0而是1
        int start = 1;
        int end = arr.length - 1;
        while (end >= start) {
            int middle = (end - start) / 2 + start;
            int count = countRange(arr, start, middle);
            //当扫描到最后一次即二分后的数组只有一个数字了
            if (end == start) {
                //发现超过一个数与此数相等,则表示这个数是重复数字
                if (count > 1) {
                    return start;
                }
            } else {
                //前面这一半必然存在一个重复数字
                if (count > (middle - start + 1)) {
                    //让扫描的结尾数字等于这个middle
                    end = middle;
                } else {
                    start = middle + 1;
                }
            }
        }
        return -1;
    }

    private int countRange(int[] arr, int start, int end) {
        int count = 0;
        for (int j : arr) {
            //如果扫描到的数在这个二分区间中,让count++
            if (j >= start && j <= end) {
                count++;
            }
        }
        return count;
    }

需要指出的是,这种算法不能保证找出所有重复的数字。例如,该算法不能找出数组{2,3,5,4,3,2,6,7}中重复的数字2。这是因为在1~2的范围里有12两个数字,这个范围的数字也出现2次,此时我们用该算法不能确定是每个数字各出现一次还是某个数字出现了两次。

04_二维数组中的查找

题目描述

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

现有矩阵 matrix 如下:

[
  [1, 2,  8,  9],
  [2, 4,  9, 12],
  [4, 7, 10, 13],
  [6, 8, 11, 15]
]

给定 target = 5,返回 true。给定 target = 20,返回 false

限制:0 <= n <= 1000 && 0 <= m <= 1000

解法

我们可以从数组的左上角选取一个数字来和要查询的数字进行比较

首先我们选取数组右上角数字9,由于9大于7,并且9还是第4列的第一个(也是最小的)数字,因此7不可能出现在数字9所在的列。于是我们把这一列从需要考虑的区域内剔除,之后只需要分析剩下的3列。

image-20210303131202699

在剩下的矩阵中,位于右上角的数字是8。同样8大于7,因此8所在的列我们也可以剔除。接下来我们只要分析剩下的两列即可。

image-20210303131308641

在由剩余的两列组成的数组中,数字2位于数组的右上角。2小于7,那么要查找的7可能在2的右边,也可能在2的下边。在前面的步骤中,我们已经发现2右边的列都已经被剔除了,也就是说7不可能出现在2的右边,因此7只有可能出现在2的下边。于是我们把数字2所在的行也剔除,只分析剩下的三行两列数字。

image-20210303131413828

在剩下的数字中,数字4位于右上角,和前面一样,我们把数字4所在的行也删除,最后剩下两行两列数字

image-20210303131438381

在剩下的两行两列4个数字中,位于右上角的刚好就是我们要查找的数字7,于是查找过程就可以结束了。

class Solution {
    public boolean findNumberIn2DArray(int[][] matrix, int target) {
        boolean flag = false;
        if (matrix.length == 0 || matrix[0].length == 0) {
            return flag;
        }
        //行
        int row = 0;
        //列
        int col = matrix[0].length - 1;
        while (row < matrix.length && col >= 0) {
            if (matrix[row][col] == target) {
                flag = true;
                break;
            } else if (matrix[row][col] > target) {
                col--;
            } else {
                row++;
            }
        }
        return flag;
    }
}

05_替换空格

题目描述

  • 请实现一个函数,把字符串 s 中的每个空格替换成"%20"

示例

输入:s = "We are happy."
输出:"We%20are%20happy."

限制

0 <= s 的长度 <= 10000

解法A

现在我们考虑怎么执行替换操作。最直观的做法是从头到尾扫描字符串,每次碰到空格字符的时候进行替换。由于是把1个字符替换成3个字符,我们必须要把空格后面所有的字符都后移字节,否则就有两个字符被覆盖了。
举个例子,我们从头到尾把"We are happy.“中的每个空格替换成”%20" 。为了形象起见,我们可以用一个表格来表示字符串,表格中的每个格子表示一个字符,如图所示。

image-20210303170434815

class Solution {
    public String replaceSpace(String s) {
        char[] arr = new char[s.length() * 3];
        char c;
        int count = 0;
        for (int i = 0; i < s.length(); i++) {
            c = s.charAt(i);
            if (' ' != c) {
                arr[i + count] = c;
            } else {
                arr[i + count] = '%';
                arr[i + count + 1] = '2';
                arr[i + count + 2] = '0';
                count += 2;
            }
        }
        return new String(arr, 0, s.length() + count);
    }

解法B

我们可以先遍历一次字符串,这样就能统计出字符串中空格的总数,并可以由此计算出替换之后的字符串的总长度。每替换一个空格,长度增加2,因此替换

以后字符串的长度等于原来的长度加上2乘以空格数目。我们还是以前面的字符串"We are happy."为例。"We are happy."这个字符串的长度是14(包括

结尾符号"0’),里面有两个空格,因此替换之后字符串的长度是18

我们从字符串的后面开始复制和替换。首先准备两个指针:P1P2P1指向原始字符串的末尾,而P2指向替换之后的字符串的末尾,接下来

我们向前移动指针P1,逐个把它指向的字符复制到P指向的位置,直到碰到第一个空格为止。碰到第一个空格之后把P1向前移动1格,在P2之前插入字符

串"%20"。由于"%20"的长度为同时也要把P2向前移动3格,我们接着向前复制,直到碰到第二个空格,和上一次一样,我们再把P1向前移动1格,并把P2

向前移动3格插入"%20"。此时P1P2指向同一位置,表明所有空格都已经替换完毕。

image-20210303174807245

06_从头到尾打印链表

题目描述

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

输入

{67,0,24,58}

返回值

[58,24,0,67]

栈写法

public static ArrayList<Integer> printListFromTailToHeadStack(ListNode listNode) {
    //直接实用java库的栈
    Stack<Integer> stack = new Stack<>();
    ArrayList<Integer> arrayList = new ArrayList<>();
    while (listNode != null) {
        //入栈
        stack.add(listNode.val);
        listNode = listNode.next;
    }
    while (!stack.isEmpty()) {
        //出栈,放入集合
        arrayList.add(stack.pop());
    }
    return arrayList;
}

递归写法

public static ArrayList<Integer> printListFromTailToHeadRecursion(ListNode listNode) {
    ArrayList<Integer> arrayList = new ArrayList<>();
    if (listNode == null) {
        return arrayList;
    }
    return recursion(arrayList, listNode);
}

private static ArrayList<Integer> recursion(ArrayList<Integer> arrayList, ListNode listNode) {
    if (listNode.next != null) {
        recursion(arrayList, listNode.next);
    }
    //从链表末尾开始添加数据到集合
    arrayList.add(listNode.val);
    return arrayList;
}

07_重建二叉树

二叉树的遍历

遍历:按某条搜索路径巡访树中的每一个节点,使得每一个节点均被访问一次,且仅被访问一次

image-20210313104123847

给出一个节点

static class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int x) {
        val = x;
    }
}

先序遍历

-> 左子树 -> 右子树

  • 若二叉树为空,则空操作
  • 否则
    • 访问根节点
    • 先序遍历左子树
    • 先序遍历右子树

A,B,D,E,H,I,C,F,G

/**
 * 先序遍历
 */
private static void preorderTraversal(TreeNode treeNode) {
    System.out.print(treeNode.val);
    if (treeNode.left != null) {
        preorderTraversal(treeNode.left);
    }
    if (treeNode.right != null) {
        preorderTraversal(treeNode.right);
    }
}

中序遍历

左子树 -> -> 右子树

  • 若二叉树为空,则空操作
  • 否则
    • 中序遍历左子树
    • 访问根节点
    • 中序遍历右子树

D,B,H,E,I,A,F,C,G

/**
 * 中序遍历
 */
private static void inOrderTraversal(TreeNode treeNode) {
    if (treeNode.left != null) {
        inOrderTraversal(treeNode.left);
    }
    System.out.print(treeNode.val);
    if (treeNode.right != null) {
        inOrderTraversal(treeNode.right);
    }
}

后序遍历

左子树 -> 右子树 ->

  • 若二叉树为空,则空操作
  • 否则
    • 后序遍历左子树
    • 后序遍历右子树
    • 访问根节点

D,H,I,E,B,F,G,C,A

/**
 * 后序遍历
 */
private static void postOrderTraversal(TreeNode treeNode) {
    if (treeNode.left != null) {
        postOrderTraversal(treeNode.left);
    }
    if (treeNode.right != null) {
        postOrderTraversal(treeNode.right);
    }
    System.out.print(treeNode.val);
}

二叉树遍历的经典案例

在只知道某二叉树的先序遍历序列和后序遍历序列,无法推出二叉树的结构,其他情况都可推出

已知二叉树的后序遍历序列DABEC,中序遍历序列DEBAC,求先序遍历

根据描述通用过程为:

  • 后续遍历序列的最后节点C,必然是根节点
  • 中序遍历序列中,根节点右边的节点必然在其右子树上,左边的节点必然在其左子树上,可以得出,此二叉树仅有左子树
  • 然后固定C在根节点,看其余节点,同样的方法。看新后续序列ADBE,得出左子树根节点为E
  • 中序序列节点E左边仅有一个D,也就是左子树为D,右子树为BA
  • 同样过程在后续遍历序列排除DEC,还有AB,得出B为右子树根节点,A为B子节点
  • 再看中序序列,B在A前面,所以A为右节点,得出二叉树结构

image-20210313111921481

重建二叉树

题目描述

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

输入

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

返回值

{1,2,5,3,4,6,7}

分析过程描述案例

方案A
  • 根据前序遍历得知,此二叉树根节点为1
  • 再根据中序遍历得知,4,7,2为1的左子树节点
  • 再根据前序遍历,得知1的左子树根节点为2
  • 中序遍历2的左边为4,7,也就是2的左子树节点,右边就到1了,说明没有,为空
  • 再看前序遍历,4在7前面,说明4为2的左子树根节点
  • 回到中序遍历,4在7前面,说明7为4的右叶子节点
  • 至此1的左子树排列完毕,再看中序遍历得知右子树节点有5,3,8,6
  • 根据前序遍历,得知3为1的右子树根节点
  • 看中序遍历,5在3左边,8,6在3右边,也就是3左叶子节点为3,右子树为8,6
  • 再看前序遍历,6在8前面,说明6为8的根节点,
  • 再看中序遍历,8在6前面,说明6在8的左叶子节点,至此树的结构分析出来了
方案B
  • 遍历前序序列至1,得知1为根节点,然后看中序,得知4,2,7为1的左子树节点
  • 遍历前序序列至2,得出2为1左子树根节点,然后看中序,2左边有4,7,右边为1(无右子树)
  • 遍历前序序列至4,得出4为2左子树根节点,然后看中序,4左边无节点,右边有7,表示7为4的右叶子节点,至此1的左子树分析完毕
  • 继续遍历前序序列至3(7已经分析过,跳过),得出3为1右子树根节点,然后看中序,3左边有5,右边有8,6
  • 遍历前序序列至5,得出5为3的左子树根节点,然后看中序,5左右均无没有出现过的元素,得出5为叶子节点
  • 遍历前序序列至6,得出6为3的右子树根节点,然后看中序,得出6无右子树,只有左子树8
  • 遍历前序序列至8,得出8为6的左子树,且为叶子节点,至此树的结构分析出来了

image-20210313113105234

为了让自己下次看的时候能清晰一些,所以写的有点啰嗦了

代码实现

根据上面的方案B实现

public class Solution {
    private static int index;
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;

        TreeNode(int x) {
            val = x;
        }
    }
    private TreeNode recursion(int[] pre, int[] in) {
        //当前节点的左子树的节点个数
        int len1 = 0;
        //当前节点的右子树的节点个数
        int len2;
        /*
         * pre[index]表示从先序遍历序列遍历的数,每次index要加一,对于中序遍历序列,
         * pre[index]这个数左边必然是他的左子树节点,右边则是右子树节点
         */
        for (int j : in) {
            if (pre[index] == j) {
                break;
            }
            //左子树节点个数加1
            len1++;
        }
        //右子树节点可以根据总节点数量减去左子树节点再减根节点
        len2 = in.length - len1 - 1;

        int index1 = 0;
        int index2 = 0;
        //当前节点的左子树,大小为节点个数
        int[] temp1 = new int[len1];
        //当前节点的右子树,大小为节点个数
        int[] temp2 = new int[len2];

        //标志位,用于决定往左子树还是右子树节点数组添加当前节点
        boolean flag = false;
        for (int j : in) {
            //左子树节点收集完毕
            if (pre[index] == j) {
                //标志
                flag = true;
            } else if (!flag) {
                //左子树节点
                temp1[index1++] = j;
            } else {
                //右子树节点
                temp2[index2++] = j;
            }
        }

        //初始根节点值为先序遍历序列第一个数,第二次则为第二个数,第三次。。。
        TreeNode treeNode = new TreeNode(pre[index]);
        treeNode.left = null;
        treeNode.right = null;
        //将左子树节点的数组和先序遍历序列进行递归操作
        if (temp1.length > 0) {
            //先序遍历序列索引加一
            index++;
            //左子树节点
            treeNode.left = recursion(pre, temp1);
        }
        //将右子树节点的数组和先序遍历序列进行递归操作
        if (temp2.length > 0) {
            //先序遍历序列索引加一
            index++;
            //右子树节点
            treeNode.right = recursion(pre, temp2);
        }
        //返回本次操作后的的节点或子树,如果temp1和temp2都为空,则表示找到的是根节点
        return treeNode;
    }
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        if (pre == null || in == null || in.length == 0 || pre.length != in.length) {
            return null;
        }
        index = 0;
        return recursion(pre, in);
    }
}

在看题解的时候,发现一个比这个简洁不少的写法,如下

private static TreeNode reConstructBinaryTree(int[] pre, int[] in) {
    if (pre == null || in == null || in.length == 0 || pre.length != in.length) {
        return null;
    }
    TreeNode root = new TreeNode(pre[0]);
    for (int i = 0; i < pre.length; i++) {
        if (pre[0] == in[i]) {
            root.left = reConstructBinaryTree(
                /*
                         * 将前序序列去掉根结点后的序列划分为左、右两个序列,它们分别是左、右子树的前序序列,为什么要是i+1呢
                         * 就是因为中序遍历的这个节点前面有i个数,所以将前序序列从根节点下一个元素开始截取i个数作为下一轮递归的前序序列
                         */
                Arrays.copyOfRange(pre, 1, i + 1),
                /*
                         *对于中序遍历序列的第i个数字来讲,由于它是二叉树根节点,左边部分就是他的左子树,所以将其截取出来放入下一轮递归
                         */
                Arrays.copyOfRange(in, 0, i));
            root.right = reConstructBinaryTree(
                Arrays.copyOfRange(pre, i + 1, pre.length),
                Arrays.copyOfRange(in, i + 1, in.length));
        }
    }
    return root;
}

09_用两个栈实现队列

题目描述

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

代码实现

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

public void push(int node) {
    /*
     * 判断stack2中元素是否为空,不为空的情况就是在pop操作中将stack1的元素pop出push到了stack2,
     * 这个时候为了保证新插入的数据顺序正确,需要原路将stack2的数据压回stack1
     */
    while (!stack2.isEmpty()) {
        stack1.push(stack2.pop());
    }
    //执行入栈
    stack1.push(node);
}

public int pop() throws Exception {
    if (stack1.isEmpty() && stack2.isEmpty()) {
        throw new Exception("栈空");
    }
    if (stack2.isEmpty()) {
        /*
         * 如果stack2不为空,情况便是在执行了pop操作后,没有继续push,导致数据没有被压回stack2,
         * 所以可以直接执行pop,如果为空,则表示执行过push方法,栈空的情况上面判断了
         */
        while (!stack1.isEmpty()) {
            stack2.push(stack1.pop());
        }
    }
    return stack2.pop();
}

10_1、斐波那契数列

题目描述

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

n <= 39

输入

4

返回值

3

数组循环解法

public static int fibonacci(int n) {
    if (n == 0) {
        return 0;
    }
    if (n == 1 || n == 2) {
        return 1;
    }
    int[] arr = new int[n + 1];
    arr[0] = 0;
    arr[1] = 1;
    arr[2] = 1;
    for (int i = 3; i <= n; i++) {
        arr[i] = arr[i - 1] + arr[i - 2];
    }
    return arr[n];
}

递归解法

public static int fibonacci(int n) {
    if (n == 0) {
        return 0;
    }
    if (n == 1 || n == 2) {
        return 1;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

临时变量保存解法

public static int fibonacci(int n) {
    if (n == 0) {
        return 0;
    }
    if (n == 1 || n == 2) {
        return 1;
    }
    int a = 0;
    int b = 1;
    int fibNum = 0;
    for (int i = 2; i <= n; i++) {
        //前两个数之和
        fibNum = a + b;
        //模拟向右移动a变为b
        a = b;
        //b变为计算出来的fibNum
        b = fibNum;
    }
    return fibNum;
}

10_2、青蛙跳台阶问题

题目描述

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

输入

1

返回值

1

输入

4

返回值

5

递归操作

青蛙从第一级开始跳到第n级台阶的这个过程,我们反过来看,我们可以从第n级开始往第一级台阶跳,每跳一次,可能会跳1级或者2级,那么我们就分别求出跳1级后可能会出现的情况和跳2级后可能出现的情况,在各自的场景中,各自又可以从当前台阶跳一级或者两级台阶,这个过程我们可以采用递归的方式,递归终止条件明显就是当跳到第一级台阶的时候就结束了

image-20210314114626856

public static int jumpFloor(int target) {
    if (target == 1) {
        //只有一级,那肯定就为1。
        return 1;
    }
    if (target == 2) {
        //要么一级一级跳 = jumpFloor(1),要么一次性两级 = 1
        return 1 + jumpFloor(1);
    }
    return jumpFloor(target - 1) + jumpFloor(target - 2);
}

数组操作

public static int jumpFloor(int target) {
    if (target == 1) {
        return 1;
    }
    if (target == 2) {
        return 2;
    }
    int[] arr = new int[target + 1];
    arr[1] = 1;
    arr[2] = 2;
    for (int i = 3; i <= target; i++) {
        arr[i] = arr[i - 2] + arr[i - 1];
    }
    return arr[target];
}

跳台阶扩展问题

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

输入

3

返回值

4

对于开始的只能跳1或者2级,我们在递归的时候是用的n-1和n-2两种情况的跳法之和求得n的跳法数,现在是取消了这个限制,可以跳1n级,道理是一样的,我们就可以在递归的时候求出1n-1这n-2种情况的跳法之和。

public static int jumpFloorII(int target) {
    if (target == 0) {
        return 0;
    }
    if (target == 1) {
        return 1;
    }
    //因为直接从起点跳到终点也是可以的,需要把这种特殊情况加上,所以初始赋值为1
    int count = 1;
    for (int i = 1; i < target; i++) {
        //递归
        count += jumpFloorII(i);
    }
    return count;
}

递归耗费内存十分巨大,所以也实现了纯数组方式解决,同样,开始只能1或者2级跳,在遍历的时候直接将当前下标的前两个数赋值给当前下标(也就是当前位置只能是前一个台阶或者前一个的前一个台阶跳过来的),现在由于是1~n级都可以跳,那就是前面任何位置都可能跳过来,仅仅就是把求前两个数的和变为从0到当前下标的前一个位置遍历求和。

public static int jumpFloorII(int target) {
    if (target == 0) {
        return 0;
    }
    if (target == 1) {
        return 1;
    }
    int[] arr = new int[target + 1];
    arr[0] = 0;
    arr[1] = 1;
    for (int i = 2; i <= target; i++) {
        int count = 1;
        for (int j = 0; j < i; j++) {
            count += arr[j];
        }
        arr[i] = count;
    }
    return arr[target];
}

10_3、矩阵覆盖

题目描述

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

比如n=3时,2*3的矩形块有3种覆盖方法:

img

输入

4

返回值

5

解析

假设现在给出的矩阵为2*8,覆盖方法记为f(8)。用一个2*1的小矩形从左到右覆盖,有两种选择,竖着放或者横着放。当竖着放的时候,右边还剩下2*7的区域,那么剩下的覆盖方法记为f(7)。当横着放的时候,2*1的小矩形放在左上角或者左下角,另一个角都需要再放一个2*1的矩形,那么右边就还剩下2*6的区域,这种情况又可以记为f(6),所以可以得出f(8) = f(7) + f(6),可以看出,这仍然是一个斐波那契数列

代码

public static int rectCover(int n) {
    //无法构成矩形
    if (n == 0) {
        return 0;
    }
    if (n == 1) {
        return 1;
    }
    int a = 1;
    int b = 1;
    int fibNum = 0;
    for (int i = 2; i <= n; i++) {
        fibNum = a + b;
        b = a;
        a = fibNum;
    }
    return fibNum;
}

11、旋转数组的最小数字

题目描述

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

输入

[3,4,5,1,2]

返回值

1

解析

第一种方法可以用二分法解这个问题,由于是个非递减数组旋转而成的,所以可以把数组看成两个非递减数组;设置两个标志位left,right,分别指向数组开头和结尾,取两者的中间数middle,比较数组下标middle和right两个位置的数,如果middle>right,说明当前middle应该在最小数应该在middle的右边,就可以将left标志位置为middle+1,否则就应该在middle的左边,置right为middle+1。第二种就是直接无脑遍历找最小数了。

无脑遍历

public static int minNumberInRotateArray(int[] array) {
    int len = array.length;
    if (len == 0) {
        return 0;
    }
    for (int i = 0; i < array.length; i++) {
        if (array[i] > array[i + 1]) {
            return array[i + 1];
        }
    }
    return array[len - 1];
}

二分法

public static int minNumberInRotateArray(int[] array) {
    if (array.length == 0) {
        return 0;
    }
    int left = 0;
    int right = array.length - 1;
    while (left < right) {
        int mid = (right + left) >> 1;
        if (array[mid] < array[right]) {
            right = mid;
        } else if (array[mid] > array[right]) {
            left = mid + 1;
        } else {
            right--;
        }
    }
    return array[left];
}

12. 矩阵中的路径

题目描述

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。

[[“a”,“b”,“c”,“e”],
[“s”,“f”,“c”,“s”],
[“a”,“d”,“e”,“e”]]

但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。

示例 1:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

示例 2:

输入:board = [["a","b"],["c","d"]], word = "abcd"
输出:false

提示:

  • 1 <= board.length <= 200
  • 1 <= board[i].length <= 200

解析

这道题是应用回溯法的经典案例

由于矩阵的第一行第二个字母’b’和路径"bfce"的第一个字符相等,我们就从这里开始分析。根据题目的要求,我们此时有3个选项,分别是向左到达字母’a’、

向右到达字母’r’、向下到达字母’f’。我们先尝试选项’a’,由于此时不可能得到路径"bfce",因此不得不回到节点’b’尝试下一个选项t。同样,经过节点’r’也不

可能得到路径"bfce",因此再次回到节点’b’尝试下一个选项’f’。

在节点’r’我们也有3个选项,向左、向右都能到达字母’c’、.向下到达字母’d’。我们先选择向左到达字母’c’,此时只有一个选择,即向下到达字母’y’。由于此时

的路径为"bfcj",不满足题目的约束条件,我们只好回到上一个节点左边的节点’c’。注意到左边的节点’c’的所有选项都已经尝试过,因此只好再回溯到上一

个节点’r’并尝试它的下一个选项,即向右到达节点’c’。

在右边的节点’c’我们有两个选择,即向右到达节点’s’,或者向下到达节点’e’。由于经过节点’s’不可能找到满足条件的路径,我们再选择节点’e’,此时路径上的字母刚好组成字符串"bfce",满足题目的约束条件,因此我们找到了符合要求的解决方案,如图所示。

image-20210315101936033

public boolean exist(char[][] board, String word) {
    //由于已经访问过的格子不能再访问,定义一个标志位二维数组
    boolean[][] vis = new boolean[board.length][board[0].length];
    //这个for循环有效执行仅仅是匹配到word的首字母,然后进入find方法开始回溯判断。
    for (int i = 0; i < board.length; i++) {
        for (int j = 0; j < board[i].length; j++) {
            if (find(board, word, i, j, vis, 0)) {
                //当且仅当走到word的最后一个字符且相等,find方法才会返回true。
                return true;
            }
        }
    }
    return false;
}

private boolean find(char[][] board, String word, int i, int j, boolean[][] vis, int index) {
    //当坐标超出了二维数组的返回,返回false,当这个坐标的格子已经访问过,返回false
    if (i < 0 || j < 0 || i >= board.length || j >= board[0].length || vis[i][j]) {
        return false;
    }
    //如果当前格子字符不等于word对应位置字母,则返回false
    if (word.charAt(index) != board[i][j]) {
        return false;
    }
    //如果index已经到了word的最后一位,而且,上一个if判断两个字符是否相等也通过,则找到了这个单词,返回true
    if (index == word.length() - 1) {
        return true;
    }
    //如果还没到最后一位,而且位置坐标也合法,就将这个格子的上下左右节点进行下一轮判断,将当前格子访问状态置为true表示已经访问
    vis[i][j] = true;
    /*
     * 上下左右判断,只要没有到最后一位,且位置合法,程序会不停的递归调用这里(当然也不是一直调用,上面有一个判断当前节点和word的对应字符是否
     * 相等,这个判断就决定了这个递归大部分情况只能递归一层,只有成功的路径才会深入),每次递归到新的节点的时候,都应该将该格子置为true已经访问
     */
    boolean flag =  //右
            find(board, word, i + 1, j, vis, index + 1)
                    //左
                    || find(board, word, i - 1, j, vis, index + 1)
                    //下
                    || find(board, word, i, j + 1, vis, index + 1)
                    //上
                    || find(board, word, i, j - 1, vis, index + 1);
    /*
     * 当上面的递归栈到底以后,要么true,要么false,如果true,则这里的vis[i][j]可以不做操作,直接返回true了,如果是false,
     * 则表示程序走的这条路不正确,需要重新取找,我们就应该消除这条错误路径带来的访问状态影响,把这个格子的访问状态还原为未访问
     */
    vis[i][j] = false;
    return flag;
}

13. 机器人的运动范围

题目描述

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

示例 1:

输入:m = 2, n = 3, k = 1
输出:3

示例 2:

输入:m = 3, n = 1, k = 0
输出:1

提示:

  • 1 <= n,m <= 100
  • 0 <= k <= 20

解答

和上一个题类似,直接看注释吧

/**
 * 全局变量,最后可走的格子数量体现在这个num上面
 */
private static int num = 0;

public static void movingCount(int m, int n, int k) {
    //同样是标记格子访问状态,不能重复计算
    boolean[][] visited = new boolean[m][n];
    //区别于在矩阵中找路径,其不知道路径的起点,所以需要对二维数组遍历,但是这里规定了是从0,0开始,所以不需要遍历,直接从0,0开始走即可
    judge(0, 0, m, n, k, visited);
}

/**
 * 通过两个坐标,求得两个坐标的数位之和
 */
public static int cul(int x, int y) {
    int sum = 0;
    while (x != 0) {
        sum += x % 10;
        x /= 10;
    }
    while (y != 0) {
        sum += y % 10;
        y /= 10;
    }
    return sum;
}

private static void judge(int x, int y, int rows, int cols, int k, boolean[][] visited) {
    //当x和y超出格子范围,直接返回,当格子已经被访问,直接返回,当x,y坐标数位和超过指定的数字,直接返回
    if (x < 0 || y < 0 || x >= rows || y >= cols || visited[x][y] || cul(x, y) > k) {
        return;
    }
    //满足要求,访问状态可以置为true,表示这个格子可走
    visited[x][y] = true;
    //数量加一
    num++;
    //对上下左右的格子分别递归
    judge(x + 1, y, rows, cols, k, visited);
    judge(x - 1, y, rows, cols, k, visited);
    judge(x, y + 1, rows, cols, k, visited);
    judge(x, y - 1, rows, cols, k, visited);
}

14_1、剪绳子

题目描述

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

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

提示:

  • 2 <= n <= 58

代码

动态规划解,

这题用动态规划是比较好理解的

  • 我们想要求长度为n的绳子剪掉后的最大乘积,可以从前面比n小的绳子转移而来
  • 用一个dp数组记录从0到n长度的绳子剪掉后的最大乘积,也就是dp[i]表示长度为i的绳子剪成m段后的最大乘积,初始化dp[2] = 1
    我们先把绳子剪掉第一段(长度为j),如果只剪掉长度为1,对最后的乘积无任何增益,所以从长度为2开始剪
  • 剪了第一段后,剩下(i - j)长度可以剪也可以不剪。如果不剪的话长度乘积即为j * (i - j);如果剪的话长度乘积即为j * dp[i - j]。取两者最大值max(j * (i - j), j * dp[i - j])
  • 第一段长度j可以取的区间为[2,i),对所有j不同的情况取最大值,因此最终dp[i]的转移方程为
    dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]))
  • 最后返回dp[n]即可
public static int cuttingRope(int n) {
    /*
     * 用一个dp数组记录从0到n长度的绳子剪掉后的最大乘积,也就是dp[i]表示长度为i的绳子剪成m段后的最大乘积,初始化dp[2] = 1
     */
    int[] dp = new int[n + 1];
    /*
     * 绳子长度2被剪掉乘积必然为1
     */
    dp[2] = 1;
    /*
     * 外层循环将大问题一次分解为子问题处理.从3开始
     */
    for (int i = 3; i <= n; i++) {
        /*
         * 我们先把绳子剪掉第一段(长度为j),如果只剪掉长度为1,对最后的乘积无任何增益,
         * 所以从长度为2开始剪,由于绳子从中间往两边走,是对称的,只需要求前一半的绳子剪掉的成绩最大即可
         */
        for (int j = 2; j <= i / 2; j++) {
            /*
             * 剪了第一段后,剩下(i - j)长度可以剪也可以不剪。如果不剪的话长度乘积即为j * (i - j);
             * 如果剪的话长度乘积即为j * dp[i - j]。取两者最大值max(j * (i - j), j * dp[i - j])
             */
            dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
        }
    }
    return dp[n];
}

贪心算法解,核心思路是:尽可能把绳子分成长度为3的小段,这样乘积最大

public static int cuttingRope(int n) {
    /*
     * n == 2 结果为1 * 1 = 1
     * n == 3 结果为1 * 2 = 2
     * n == 4 结果为2 * 2 = 4,根据规律,2,3可以合并
     */
    if (n < 4) {
        return n - 1;
    }
    int res = 1;
    /*
     * 尽可能多的分解出3来相乘,直至无法再分或等于4的时候
     */
    while (n > 4) {
        res *= 3;
        n -= 3;
    }
    //将n分割3剩下的乘到res中
    return res * n;
}

14_2、剪绳子

题目描述

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

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

提示:

  • 2 <= n <= 1000

代码

动态规划,思路是一样,主要是现在n的范围变大了,原来的int不满足要求,需要使用大数类型

public static int cuttingRope(int n) {
    /*
     * 用一个dp数组记录从0到n长度的绳子剪掉后的最大乘积,也就是dp[i]表示长度为i的绳子剪成m段后的最大乘积,初始化dp[2] = 1
     */
    BigInteger[] dp = new BigInteger[n + 1];
    //因为大数数组默认不初始化数据,这里将数组值全部置为1
    Arrays.fill(dp, BigInteger.valueOf(1));
    /*
     * 外层循环将大问题一次分解为子问题处理.从3开始
     */
    for (int i = 3; i <= n; i++) {
        /*
         * 我们先把绳子剪掉第一段(长度为j),如果只剪掉长度为1,对最后的乘积无任何增益,
         * 所以从长度为2开始剪,由于绳子从中间往两边走,是对称的,只需要求前一半的绳子剪掉的成绩最大即可
         */
        for (int j = 2; j <= i / 2; j++) {
            /*
             * 剪了第一段后,剩下(i - j)长度可以剪也可以不剪。如果不剪的话长度乘积即为j * (i - j);
             * 如果剪的话长度乘积即为j * dp[i - j]。取两者最大值max(j * (i - j), j * dp[i - j])
             * multiply方法会将这个大数乘以参数
             * 大数的max可以得到两个数的大值
             */
            dp[i] = dp[i].max(BigInteger.valueOf((long) j * (i - j))).max(dp[i - j].multiply(BigInteger.valueOf(j)));
        }
    }
    /*
     * 1. 1000000007是一个质数
     * 2. int32位的最大值为2147483647,所以对于int32位来说1000000007足够大
     * 3. int64位的最大值为2^63-1,对于1000000007来说它的平方不会在int64中溢出
     */
    return dp[n].mod(BigInteger.valueOf(1000000007)).intValue();
}

贪心

public static int cuttingRope(int n) {
    /*
     * n == 2 结果为1 * 1 = 1
     * n == 3 结果为1 * 2 = 2
     * n == 4 结果为2 * 2 = 4,根据规律,2,3可以合并
     */
    if (n < 4) {
        return n - 1;
    }
    long res = 1;
    /*
     * 尽可能多的分解出3来相乘,直至无法再分
     */
    while (n > 4) {
        res = res * 3 % 1000000007;
        n -= 3;
    }
    //将n分割3剩下的乘到res中
    return (int) (res * n % 1000000007);
}

15、二进制中1的个数

题目描述

请实现一个函数,输入一个整数(以二进制串形式),输出该数二进制表示中 1 的个数。例如,把 9 表示成二进制是 1001,有 2 位是 1。因此,如果输入 9,则该函数输出 2。

示例 1:

输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。

示例 2:

输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。

示例 3:

输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。

提示:

  • 输入必须是长度为 32二进制串

代码

逐位判断

根据 与运算 定义,设二进制数字 n ,则有:

若 n & 1 = 0 ,则 n 二进制 最右一位 为 0 ;

若 n & 1 = 1 ,则 n 二进制 最右一位 为 1 。

注意一点,这里移位操作需要使用无符号右移,因为对于负数而言,比如-1,它的二进制补码表示为1111…1(32位),如果有符号位右移,那么每次都会补一个1上来,导致这个32位二进制一直保持1的状态,而无符号位右移则是无论是正是负,一律补0,java中有符号位右移是">>",无符号位右移是">>>"。当然也可以通过左移来规避这个问题,但是这样就导致了不论数的大小,统统都会往左移动32位才能得到正确的答案,这样效率是很低下的。

public static int hammingWeight(int n) {
    int res = 0;
    while (n != 0) {
        res += n & 1;
        n >>>= 1;
    }
    return res;
}

(n - 1) : 二进制数字 n 最右边的 1 变成 0 ,此 1 右边的 0 都变成 1 。

n & (n - 1): 二进制数字 n 最右边的 1 变成 0 ,其余不变。

Picture10.png

public static int hammingWeight(int n) {
    int res = 0;
    while (n != 0) {
        n &= n - 1;
        res++;
    }
    return res;
}

16、数值的整数次方

题目描述

实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,x^n)。不得使用库函数,同时不需要考虑大数问题。

示例 1:

输入:x = 2.00000, n = 10
输出:1024.00000

示例 2:

输入:x = 2.10000, n = 3
输出:9.26100

示例 3:

输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25

提示:

  • -100.0 < x < 100.0
  • -231 <= n <= 231-1
  • -104 <= xn <= 104

快速幂解析

  • 二进制角度

n=9的时候有:

Picture1.png

也就是将以前的九次乘法化简为现在的四次乘法,而其中的x2可以由两个x1相乘,x4可以由两个x2相乘,x8可以由两个x4相乘得到,而这一过程很明显可以用``x *= x`这个累乘实现

换个例子,现在求x的十一次方,那么转换11的二进制形式1101,根据快速幂的思想,有:

x 11 − − − − − − − − − − > x 2 0 + 2 1 + 2 3 − − − − − − − − − − > x 2 0 ∗ x 2 1 ∗ 2 2 3 x^{11} ----------> x^{2^0 + 2^1 + 2^3} ----------> x^{2^0} * x^{2^1} * 2^{2^3} x11>x20+21+23>x20x21223

此时只运算了3次乘积,1011二进制数,从右至左分别为1 1 0 1 ,只有在1的位置上,才有相应的权重,这也就是为什么需要通过与运算:(b & 1) == 1判断最后一位是否为1。

x − − − − − − − − − − > x 2 0 − − − − − − − − − − > 1 x ----------> x^{2^0} ----------> 1 x>x20>1

x − − − − − − − − − − > x 2 1 − − − − − − − − − − > 1 x ----------> x^{2^1} ----------> 1 x>x21>1

x − − − − − − − − − − > x 2 2 − − − − − − − − − − > 0 x ----------> x^{2^2} ----------> 0 x>x22>0

x − − − − − − − − − − > x 2 3 − − − − − − − − − − > 1 x ----------> x^{2^3} ----------> 1 x>x23>1

  • 二分角度

快速幂实际上是二分思想的一种应用。

二分推导:

x n = x n / 2 ∗ x x / 2 = ( x 2 ) n / 2 x^n = x ^{n /2} * x^{x/2} = (x^2)^{n/2} xn=xn/2xx/2=(x2)n/2

令 n/2 为整数,则需要分为奇偶两种情况(设向下取整除法符号为 “//” ):

  • 偶 数 : x n = ( x 2 ) n / / 2 偶数:x^n = (x^2)^{n//2} xn=(x2)n//2

  • 奇 数 : x n = x ∗ ( x 2 ) n / / 2 , 也 就 是 会 多 出 一 项 x 奇数:x^n = x *(x^2)^{n//2},也就是会多出一项x xn=x(x2)n//2x

幂结果的推导

根 据 二 分 推 导 , 可 通 过 循 环 x = x 2 操 作 , 每 次 把 幂 从 n 降 至 n / / 2 , 直 至 将 幂 降 为 0 ; 根据二分推导,可通过循环 x = x^2操作,每次把幂从 n 降至 n//2 ,直至将幂降为 0 ; x=x2nn//20

$$
设 res=1,则初始状态 x^n = x^n 。在循环二分时,每当 n 为奇数时,将多出的一项 x 乘入 res ,则最终可化至 x^n = x^0

  • res=res ,返回 res 即可。
    $$

Picture2.png

转化为位运算:
向下整除 n//2 等价于 右移一位 n >> 1
取余数 n % 等价于 判断二进制最右一位值 n & 1

代码

public static double myPow(double x, int n) {
    if (x == 0) {
        return 0;
    }
    /*
     *  int32 变量 n∈[−2147483648,2147483647] ,因此当 n = -2147483648
     * 时执行 n = -n 会因越界而赋值出错。解决方法是先将 n 存入 long 变量 b ,后面用 b 操作即可。
     */
    long b = n;
    double res = 1.0;
    //负数的幂转换为正数
    if (b < 0) {
        //取倒数
        x = 1 / x;
        //幂变号
        b = -b;
    }
    while (b > 0) {
        /*
         * 控制权重为1的才乘,对于偶数,第一次进入这儿就跳过了,多余的x自然就没有被乘到res中,
         * 对于奇数,第一次就能进入,进入就把多余的x乘入了res中。
         */
        if ((b & 1) == 1) {
            res *= x;
        }
        //累乘
        x *= x;
        //向下整除
        b >>= 1;
    }
    return res;
}

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

题目描述

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

示例 1:

输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]

提示:

  • 用返回一个整数列表来代替打印
  • n 为正整数

解析

实际上,本题的主要考点是大数越界情况下的打印(不用无脑循环)。需要解决以下三个问题:

  • 表示大数的变量类型:
    无论是 short / int / long … 任意变量类型,数字的取值范围都是有限的。因此,大数的表示应用字符串 String 类型。

  • 生成数字的字符串集

    • 使用 int 类型时,每轮可通过 +1 生成下个数字,而此方法无法应用至 String 类型。并且, String 类型的数字的进位操作效率较低,例如 “9999” 至 “10000” 需要从个位到千位循环判断,进位 4 次。
    • 观察可知,生成的列表实际上是 n 位 0 - 9 的 全排列 ,因此可避开进位操作,通过递归生成数字的 String 列表。
  • 递归生成全排列:
    基于分治算法的思想,先固定高位,向低位递归,当个位已被固定时,添加数字的字符串。例如当 n = 2时(数字范围 1 - 99 ),固定十位为 0 - 9 ,按顺序依次开启递归,固定个位 0 - 9 ,终止递归并添加数字字符串。

Picture1.png

代码

StringBuilder res;
int n;
char[] num, loop = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
public String printNumbers(int n) {
    this.n = n;
    // 数字字符串集
    res = new StringBuilder();
    // 定义长度为 n 的字符列表
    this.num = new char[n];
    // 开启全排列递归
    dfs(0);
    // 删除最后多余的逗号
    res.deleteCharAt(res.length() - 1);
    // 转化为字符串并返回
    return res.toString();
}

void dfs(int x) {
    // 终止条件:已固定完所有位
    if (x == n) {
        // 拼接 num 并添加至 res 尾部,使用逗号隔开
        res.append(String.valueOf(num)).append(",");
        return;
    }
    // 遍历 ‘0‘ - ’9‘
    for (char i : loop) {
        // 固定第 x 位为 i
        num[x] = i;
        // 开启固定第 x + 1 位
        dfs(x + 1);
    }
}

image-20210316111651990

可以看出,代码还有问题,就是高位有多余的0

  • 删除高位多余的 0 :
    字符串左边界定义: 声明变量 start 规定字符串的左边界,以保证添加的数字字符串 num[start] 中无高位多余的 0 。例如当 n=2 时, 1−9 时 start = 1, 10−99 时 start = 0
StringBuilder res;
int nine = 0, start, n;
char[] num, loop = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};

public String printNumbers(int n) {
    this.n = n;
    res = new StringBuilder();
    num = new char[n];
    start = n - 1;
    dfs(0);
    res.deleteCharAt(res.length() - 1);
    return res.toString();
}

void dfs(int x) {
    if (x == n) {
        //将s切割出他本身的位数,如9是1位数,start就是1,就是将09切割成9赋值给s
        String s = String.valueOf(num).substring(start);
        if (!"0".equals(s)) {
            res.append(s).append(",");
        }
        //表示当前位数数字已经拼接完毕(也就是所有位数都为9),需要到下一位数开始拼接
        if (n - start == nine) {
            start--;
        }
        return;
    }
    //固定当前位
    for (char i : loop) {
        if (i == '9') {
            nine++;
        }
        num[x] = i;
        //递归拼接下一位
        dfs(x + 1);
    }
    //回溯前要恢复nine
    nine--;
}

题目要求输出的是int数组,稍作修改

int[] res;
int nine = 0, start, n, count = 0;
char[] num, loop = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};

public int[] printNumbers(int n) {
    this.n = n;
    res = new int[(int) Math.pow(10, n) - 1];
    num = new char[n];
    start = n - 1;
    dfs(0);
    return res;
}

void dfs(int x) {
    if (x == n) {
        String s = String.valueOf(num).substring(start);
        if (!"0".equals(s)) {
            res[count++] = Integer.parseInt(s);
        }
        if (n - start == nine) {
            start--;
        }
        return;
    }
    for (char i : loop) {
        if (i == '9') {
            nine++;
        }
        num[x] = i;
        dfs(x + 1);
    }
    nine--;

18、删除链表的节点

题目描述

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。

返回删除后的链表的头节点。

示例 1:

输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]

提示:

  • 用返回一个整数列表来代替打印
  • n 为正整数

代码

public ListNode deleteNode(ListNode head, int val) {
    if (head == null) {
        return null;
    }
    //将头节点记录下来,方便最后返回
    ListNode temp = new ListNode(-1);
    //操作不能直接在temp中进行,temp是用来最后返回的
    ListNode cur = temp;
    //将添加的节点放在head前面
    temp.next = head;
    //循环遍历
    while (cur.next != null) {
        if (cur.next.val == val) {
            //如果当前为最后一个节点,则它没有下一个节点,直接置为空返回即可
            if (cur.next.next == null) {
                cur.next = null;
                break;
            } else {
                cur.next = cur.next.next;
            }
        }
        cur = cur.next;
    }
    return temp.next;
}

19、正则表达式匹配

题目描述

请实现一个函数用来匹配包含’. ‘和’‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指

字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但与"aa.a"和"ab*a"均不匹配。

示例 1:

输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。

示例 2:

输入:
s = "aa"
p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:

输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。

示例 4:

输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。

示例 5:

输入:
s = "mississippi"
p = "mis*is*p*."
输出: false

提示:

  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母以及字符 .*,无连续的 '*'

解析

恶心他爸给恶心开门,恶心到家了,还是来看力扣上的大神解析:

假设主串为 A,模式串为 B 从最后一步出发,需要关注最后进来的字符。假设 A 的长度为 n ,B 的长度为 m ,关注正则表达式 B 的最后一个字符是谁,它有三种可能,‘正常字符’、’*'和 ‘.’,那针对这三种情况讨论即可,如下:

  • 如果 B 的最后一个字符是正常字符,那就是看 A[n-1] 是否等于 B[m-1],相等则看
    A 0 … … A n − 2 与 B 0 … … B m − 2 {A_0}……{A_{n-2}}与{B_0}……{B_{m-2}} A0An2B0Bm2
    的关系,不等则是不能匹配,这是子问题

  • 如果 B的最后一个字符是.,它能匹配任意字符,直接看
    A 0 … … A n − 2 与 B 0 … … B m − 2 {A_0}……{A_{n-2}}与{B_0}……{B_{m-2}} A0An2B0Bm2

    的关系

  • 如果 B的最后一个字符是*它代表 B[m-2]=c 可以重复0次或多次,它们是一个整体 c∗

    • 情况一:A[n-1] 是 0 个 c,B 最后两个字符废了,能否匹配取决于
      A 0 … … A n − 1 与 B 0 … … B m − 3 {A_0}……{A_{n-1}}与{B_0}……{B_{m-3}} A0An1B0Bm3

    • 情况二:A[n-1] 是多个 c 中的最后一个(这种情况必须 A[n-1]=c 或者 c=’.’ ),所以 A 匹配完往前挪一个,B 继续匹配,因为可以匹配多个,继续看
      A 0 … … A n − 2 与 B 0 … … B m − 1 {A_0}……{A_{n-2}}与{B_0}……{B_{m-1}} A0An2B0Bm1

方程转移

f[i][j] 代表 A 的前 i 个和 B 的前 j 个能否匹配

  • 对于前面两个情况,可以合并成一种情况
    f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] f[i][j] = f[i-1][j-1] f[i][j]=f[i1][j1]

  • 对于第三种情况,对于 c*∗ 分为看和不看两种情况

    • 不看:直接砍掉正则串的后面两个
      f [ i ] [ j ] = f [ i ] [ j − 2 ] f[i][j] = f[i][j-2] f[i][j]=f[i][j2]

    • 看:正则串不动,主串前移一个
      f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i-1][j] f[i][j]=f[i1][j]

初始条件

特判:需要考虑空串空正则

  • 空串和空正则是匹配的
    f [ 0 ] [ 0 ] = t r u e f[0][0] = true f[0][0]=true

  • 空串和非空正则,不能直接定义 true 和 false,必须要计算出来。
    比 如 A = ′ ′ , B = a ∗ b ∗ c ∗ 比如A='' ,B=a*b*c* A=,B=abc

  • 非空串和空正则必不匹配
    f [ 1 ] [ 0 ] = . . . = f [ n ] [ 0 ] = f a l s f[1][0]=...=f[n][0]=fals f[1][0]=...=f[n][0]=fals

  • 非空串和非空正则,那肯定是需要计算的了。

大体上可以分为空正则和非空正则两种,空正则也是比较好处理的,对非空正则我们肯定需要计算,非空正则的三种情况,前面两种可以合并到一起讨论,第三种情况是单独一种,那么也就是分为当前位置是 ∗ 和不是 ∗ 两种情况了。

结果

我们开数组要开 n+1,这样对于空串的处理十分方便。结果就是 f[n][m]

代码

public boolean isMatch(String s, String p) {
    int n = s.length();
    int m = p.length();
    boolean[][] f = new boolean[n + 1][m + 1];

    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            //分成空正则和非空正则两种
            if (j == 0) {
                f[i][j] = i == 0;
            } else {
                //非空正则分为两种情况 * 和 非*
                if (p.charAt(j - 1) != '*') {
                    if (i > 0 && (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.')) {
                        f[i][j] = f[i - 1][j - 1];
                    }
                } else {
                    //碰到 * 了,分为看和不看两种情况
                    //不看
                    if (j >= 2) {
                        f[i][j] |= f[i][j - 2];
                    }
                    //看
                    if (i >= 1 && j >= 2 && (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.')) {
                        f[i][j] |= f[i - 1][j];
                    }
                }
            }
        }
    }
    return f[n][m];
}

20、表示数值的字符串

题目描述

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

代码

public boolean isNumber(String s) {
    if (s == null || s.length() == 0) {
        return false;
    }
    //去掉首位空格
    s = s.trim();
    boolean numFlag = false;
    boolean dotFlag = false;
    boolean eFlag = false;
    for (int i = 0; i < s.length(); i++) {
        //判定为数字,则标记numFlag
        if (s.charAt(i) >= '0' && s.charAt(i) <= '9') {
            numFlag = true;
            //判定为.  需要没出现过.并且没出现过e
        } else if (s.charAt(i) == '.' && !dotFlag && !eFlag) {
            dotFlag = true;
            //判定为e,需要没出现过e,并且出过数字了
        } else if ((s.charAt(i) == 'e' || s.charAt(i) == 'E') && !eFlag && numFlag) {
            eFlag = true;
            numFlag = false;//为了避免123e这种请求,出现e之后就标志为false
            //判定为+-符号,只能出现在第一位或者紧接e后面
        } else if ((s.charAt(i) == '+' || s.charAt(i) == '-') && (i == 0 || s.charAt(i - 1) == 'e' || s.charAt(i - 1) == 'E')) {
            //其他情况,都是非法的
        } else {
            return false;
        }
    }
    return numFlag;
}

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

题目描述

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

示例

输入:nums = [1,2,3,4]
输出:[1,3,2,4] 
注:[3,1,2,4] 也是正确的答案之一。

提示

  • 0 <= nums.length <= 50000
  • 1 <= nums[i] <= 10000

代码

先来一手效率极低的循环。。。

public int[] exchange(int[] nums) {
    Stack<Integer> stack1 = new Stack<>();
    Stack<Integer> stack2 = new Stack<>();
    for (int num : nums) {
        if (num % 2 == 0) {
            stack1.push(num);
        } else {
            stack2.push(num);
        }
    }
    for (int i = 0; i < nums.length; i++) {
        if (!stack2.isEmpty()) {
            nums[i] = stack2.pop();
        } else {
            nums[i] = stack1.pop();
        }
    }
    return nums;
}

首尾双指针

img

public int[] exchange(int[] nums) {
    int left, right;
    left = 0;
    right = nums.length - 1;
    while (left < right) {
        //向右找到第一个偶数
        if (nums[left] % 2 == 1) {
            left++;
            continue;
        }
        //向左找到第一个奇数
        if (nums[right] % 2 == 0) {
            right--;
            continue;
        }
        //当两个都找到后,就不会执行到continue语句,执行下面的交换
        nums[left] = nums[left] ^ nums[right];
        nums[right] = nums[left] ^ nums[right];
        nums[left] = nums[left] ^ nums[right];
        left++;
        right--;
    }
    return nums;
}

快慢双指针

img

public static int[] exchange(int[] nums) {
    int fast = 0, slow = 0;
    while (fast < nums.length) {
        if (nums[fast] % 2 == 1) {
            //这里我用异或交换值数组中的1莫名其妙会变成0,换临时变量交换倒没问题
            int temp = nums[fast];
            nums[fast] = nums[slow];
            nums[slow] = temp;
            slow++;
        }
        fast++;
    }
    return nums;
}

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

描述

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

示例

给定一个链表: 1->2->3->4->5, 和 k = 2.

返回链表 4->5.

代码

这道题很简单,需要倒数某个节点,定义两个节点,只需要保证前后两个节点的位置差为这个数就可以

public ListNode getKthFromEnd(ListNode head, int k) {
    ListNode slow = head;
    ListNode fast = head;
    while (fast != null) {
        if (k == 0) {
            slow = slow.next;
        }
        if (k > 0) {
            k--;
        }
        fast = fast.next;
    }
    return slow;
}

24、反转链表

描述

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

限制

  • 0 <= 节点个数 <= 5000

代码

img

public static ListNode reverseList(ListNode head) {
    //特值判断
    if (head == null) {
        return null;
    }
    //走在前面的指针
    ListNode front = head.next;
    //比前指针少一个节点
    ListNode behind = head;
    //在用front往回behind指的时候需要临时保存front.next,不然就找不到后面的节点了
    ListNode temp;
    while (front != null) {
        //保存下一个节点
        temp = front.next;
        //当前节点next指向上一个,达到反转的作用
        front.next = behind;
        //两个指针分别向后移
        behind = front;
        front = temp;
    }
    //因为头指针的next没有消除,当反转完成后,它应该作为尾节点,next应该为null,否则在取值会出现死循环
    head.next = null;
    return behind;
}

25、合并两个排序的链表

描述

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

示例

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

限制

  • 0 <= 链表长度 <= 1000

代码

引入一个伪头节点作为新链表(实际上他的节点都是由原两个链表中的节点所组成)的头节点,注释在代码中,直接看代码吧

Picture17.png

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    //伪头节点
    ListNode head = new ListNode(-1);
    //伪头节点最后需要输出,不能动它,用一个临时节点代替它移动
    ListNode temp = head;
    while (l1 != null && l2 != null) {
        //小的先放,大的后方
        if (l1.val < l2.val) {
            temp.next = l1;
            //小的节点接上后需要移动到下一个节点
            l1 = l1.next;
        } else {
            temp.next = l2;
            l2 = l2.next;
        }
        temp = temp.next;
    }
    //最后会出现某一个链表为空,另一个还剩一个或一个以上的节点,需要将其拼接上
    if (l1 == null) {
        temp.next = l2;
    } else {
        temp.next = l1;
    }
    return head.next;
}

26、树的子结构

描述

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

B是A的子结构, 即 A中有出现和B相同的结构和节点值。

例如:
给定的树 A:

     3
    / \
   4   5
  / \
 1   2

给定的树 B:

   4 
  /
 1

返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。

示例1

输入:A = [1,2,3], B = [3,1]
输出:false

示例2

输入:A = [3,4,5,1,2], B = [4,1]
输出:true

限制

  • 0 <= 节点个数 <= 10000

代码

注释很清晰

public static boolean isSubStructure(TreeNode a, TreeNode b) {
    if (a == null || b == null) {
        return false;
    }
    /*
     * 判断是否是子树有三种情况,
     *  第一种就是这颗树的根节点出发的子树
     *  第二种就是从这颗树的左节点出发的子树
     *  第三种就是从这颗树的右节点出发的子树
     */
    return judge(a, b) || isSubStructure(a.left, b) || isSubStructure(a.right, b);
}


public static boolean judge(TreeNode a, TreeNode b) {
    //要判断的子树b都已经遍历完了,自然就相等了
    if (b == null) {
        return true;
    }
    //如果b不为空,a为空了,或者a的值和b的值不相等,则不是子树
    if(a == null || a.val != b.val){
        return false;
    }
    //递归判断a,b的子节点
    boolean flagLeft = judge(a.left, b.left);
    boolean flagRight = judge(a.right, b.right);
    return flagLeft && flagRight;
}

27、二叉树的镜像

描述

请完成一个函数,输入一个二叉树,该函数输出它的镜像。

例如输入:

     4
   /   \
  2     7
 / \   / \
1   3 6   9

镜像输出:

     4
   /   \
  7     2
 / \   / \
9   6 3   1

示例

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

限制

  • 0 <= 节点个数 <= 1000

代码

递归,通俗易懂,不用解释

public TreeNode mirrorTree(TreeNode root) {
    if (root == null) {
        return null;
    }
    //左右子树交换
    TreeNode temp = root.left;
    root.left = root.right;
    root.right = temp;
    //对左右子树分别再执行镜像操作
    mirrorTree(root.left);
    mirrorTree(root.right);
    return root;
}

28、对称的二叉树

描述

请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。

例如,二叉树 [1,2,2,3,4,4,3] 是对称的。

    1
   / \
  2   2
 / \ / \
3  4 4  3

但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

    1
   / \
  2   2
   \   \
   3    3

示例 1:

输入:root = [1,2,2,3,4,4,3]
输出:true

示例 2:

输入:root = [1,2,2,null,3,null,3]
输出:false

限制:

  • 0 <= 节点个数 <= 1000

代码

对称二叉树定义: 对于树中 任意两个对称节点 L 和 R ,一定有:
L.val = R.val :即此两对称节点值相等。
L.left.val = R.right.val :即 L 的 左子节点 和 R 的 右子节点 对称;
L.right.val = R.left.val :即 L 的 右子节点 和 R 的 左子节点 对称。

这道题第一眼就觉得还是老问题,用递归

public boolean isSymmetric(TreeNode root) {
    //空二叉树也是对称的
    if (root == null) {
        return true;
    }
    //将左右子树进行判断是否对称
    return judge(root.left, root.right);
}

public boolean judge(TreeNode left, TreeNode right) {
    //两者同时为null,证明寻访已经完成,对称成立
    if (left == null && right == null) {
        return true;
    }
    //如果某一个节点为null,则不对称
    if (left == null || right == null) {
        return false;
    }
    //如果值不相等,则不对称
    if (left.val != right.val) {
        return false;
    }
    //递归判断两个节对称位置的节点
    return judge(left.left, right.right) && judge(left.right, right.left);
}

29、顺时针打印矩阵

描述

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。

示例 1:

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

示例 2:

输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]

限制

  • 0 <= matrix.length <= 100
  • 0 <= matrix[i].length <= 100

解析

顺时针打印满足顺序是 “从左向右、从上向下、从右向左、从下向上” 循环。重点在于打印边界的设置,且边界需要根据打印状态动态收缩,所以需要在每个方向打印的时候做三件事

  • 根据边界打印,即将元素按顺序添加至列表 res 尾部;
  • 边界向内收缩 1 (代表已被打印);
  • 判断是否打印完毕(边界是否相遇),若打印完毕则跳出。

将这个过程用表格的方式展现:

打印方向1. 根据边界打印2. 边界向内收缩3. 是否打印完毕
从左向右左边界l ,右边界 r上边界 t 加 11是否 t > b
从上向下上边界 t ,下边界b右边界 r 减 11是否 l > r
从右向左右边界 r ,左边界l下边界 b 减 11是否 t > b
从下向上下边界 b ,上边界t左边界 l 加 11是否 l > r

代码

做了注释,看着又臭又长,但是很清晰

public static int[] spiralOrder(int[][] matrix) {
    if (matrix == null) {
        return null;
    }
    if (matrix.length == 0) {
        return new int[0];
    }
    //初始左边界
    int l = 0;
    //初始右边界
    int r = matrix[0].length - 1;
    //初始上边界
    int t = 0;
    //初始下边界
    int b = matrix.length - 1;
    //用于输出的数组
    int[] res = new int[(r + 1) * (b + 1)];
    //初始化数组追加元素索引
    int index = 0;
    while (true) {
        //从左边界到右边界
        for (int i = l; i <= r; i++) {
            //横坐标不变,纵坐标加
            res[index++] = matrix[t][i];
        }
        //从左到右遍历完毕,上边界就要往下移动一位,然后开始往下遍历,遍历之前判断上边界和下边界是否合法,不合法的唯一情况就是整个数组遍历完毕了
        if (++t > b) {
            break;
        }
        //从上边界到下边界
        for (int i = t; i <= b; i++) {
            //纵坐标不变,横坐标加
            res[index++] = matrix[i][r];
        }
        //从上到下遍历完毕,右边界就要往左移动一位,然后开始往左遍历,遍历之前判断右边界和左边界是否合法,不合法的唯一情况就是整个数组遍历完毕了
        if (--r < l) {
            break;
        }
        //从右边界到左边界
        for (int i = r; i >= l; i--) {
            //横坐标不变,纵坐标加
            res[index++] = matrix[b][i];
        }
        //从右到左遍历完毕,下边界就要往上移动一位,然后开始往上遍历,遍历之前判断下边界和上边界是否合法,不合法的唯一情况就是整个数组遍历完毕了
        if (--b < t) {
            break;
        }
        //从下边界到上边界
        for (int i = b; i >= t; i--) {
            //纵坐标不变,横坐标加
            res[index++] = matrix[i][l];
        }
        //从下到上遍历完毕,左边界就要往右移动一位,然后开始往右遍历,遍历之前判断左边界和右边界是否合法,不合法的唯一情况就是整个数组遍历完毕了
        if (++l > r) {
            break;
        }
    }
    return res;
}

30、包含main函数的栈

描述

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

示例

MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min();   --> 返回 -3.
minStack.pop();
minStack.top();      --> 返回 0.
minStack.min();   --> 返回 -2.

提示

  • 各函数的调用总次数不超过 20000 次

代码

如果用栈去实现栈,那有啥意义呢??所以这里用List集合和栈顶指针的方式来实现,力扣提交效率就有点低了。

static class MinStack {

    ArrayList<Integer> valueStack, minStack;
    int top;

    /**
         * initialize your data structure here.
         */
    public MinStack() {
        //初始化最小值栈和栈顶指针
        valueStack = new ArrayList<>();
        minStack = new ArrayList<>();
        top = 0;
    }

    public void push(int x) {
        //前一个数的大小关系,小就填小值,大就还是以前的值
        if (valueStack.size() == 0 || x < minStack.get(top - 1)) {
            minStack.add(top, x);
        } else {
            minStack.add(top, minStack.get(top - 1));
        }
        valueStack.add(top++, x);
    }

    public void pop() {
        minStack.remove(--top);
        valueStack.remove(top);
    }

    public int top() {
        return valueStack.get(top - 1);
    }

    public int min() {
        //直接从最小值栈顶获取即可
        return minStack.get(top - 1);
    }
}

31、栈的压入,弹出序列

描述

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

示例 1:

输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1

示例 2:

输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。

提示:

  • 0 <= pushed.length == popped.length <= 1000
  • 0 <= pushed[i] < 1000
  • pushed 是 popped 的排列。

解析

举例:入栈1,2,3,4,5,出栈4,5,3,2,1

借用一个辅助的栈,遍历压栈顺序,先讲第一个放入栈中,这里是1,然后判断栈顶元素是不是出栈顺序的第一个元素,这里是4,很显然1≠4,所以我们继续

压栈,直到相等以后开始出栈,出栈一个元素,则将出栈顺序向后移动一位,直到不相等,这样循环等压栈顺序遍历完成,如果辅助栈还不为空,说明弹出序

列不是该栈的弹出顺序。

代码

Stack<Integer> stack = new Stack<>();

public boolean validateStackSequences(int[] pushed, int[] popped) {
    if (pushed.length != popped.length) {
        return false;
    }
    //pop序列元素下标
    int index = 0;
    for (int j : pushed) {
        //入栈
        stack.push(j);
        //循环判断当前栈顶元素和出栈序列元素是否相等,相等则此片段符合序列
        while (!stack.isEmpty() && stack.peek() == popped[index]) {
            stack.pop();
            index++;
        }
    }
    return stack.isEmpty();
}

32_1、从上到下打印二叉树

描述

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

如:
给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回:

[3,9,20,15,7]

提示:

  • 节点总数 <= 1000

解析

从上到下打印二叉树也就是按层遍历,这种方式被称为 广度优先搜索(BFS),广度优先搜索通常用队列特性来实现,其过程为:

  • 当队列 queue 为空时跳出;
  • 出队: 队首元素出队,记为 node;
  • 打印: 将 node.val 添加至列表 tmp 尾部;
  • 添加子节点: 若 node 的左(右)子节点不为空,则将左(右)子节点加入队列 queue ;

代码

public int[] levelOrder(TreeNode root) {
    if (root == null) {
        return new int[0];
    }
    Queue<TreeNode> queue = new LinkedList<>();
    ArrayList<Integer> ans = new ArrayList<>();
    //将根节点放入队列,因为要从上到下遍历嘛
    queue.offer(root);
    //队列为空则遍历结束
    while (!queue.isEmpty()) {
        //将队首元素弹出
        TreeNode treeNode = queue.poll();
        //弹出的节点的值放在遍历集合中
        ans.add(treeNode.val);
        //将此节点的左节点放入队列进行下一轮遍历
        if (treeNode.left != null) {
            queue.add(treeNode.left);
        }
        //然后再将此节点的右节点放入队列进行下一轮遍历
        if (treeNode.right != null) {
            queue.add(treeNode.right);
        }
    }
    //集合转换为int[]的过程
    Integer[] arr = new Integer[ans.size()];
    ans.toArray(arr);
    int[] res = new int[arr.length];
    for (int i = 0; i < arr.length; i++) {
        res[i] = arr[i];
    }
    return res;
}

32_2、从上到下打印二叉树

描述

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。

例如:
给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
 /    \
15     7

返回其层次遍历结果:

[
  [3],
  [9,20],
  [15,7]
]

提示

  • 节点总数 <= 1000

代码

public List<List<Integer>> levelOrder(TreeNode root) {
    if (root == null) {
        return null;
    }
    Queue<TreeNode> queue = new LinkedList<>();
    List<List<Integer>> ans = new ArrayList<>();
    //将根节点放入队列,因为要从上到下遍历嘛
    queue.offer(root);
    //队列为空则遍历结束
    while (!queue.isEmpty()) {
        List<Integer> temp = new ArrayList<>();
        //特别注意这个地方,i如果从0开始循环,i<queue.size会动态变化,导致结果错误,这里需要避开这种影响,应该只在第一次循环涉及到size
        for (int i = queue.size(); i > 0; i--) {
            TreeNode treeNode = queue.poll();
            temp.add(treeNode.val);
            if (treeNode.left != null) {
                queue.offer(treeNode.left);
            }
            if (treeNode.right != null) {
                queue.offer(treeNode.right);
            }
        }
        ans.add(temp);
    }
    return ans;
}

32_3、从上到下打印二叉树

描述

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

例如:
给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其层次遍历结果:

[
  [3],
  [20,9],
  [15,7]
]

提示:

  • 节点总数 <= 1000

代码

承认取巧了,用了工具类将偶数层的temp倒置一下

public static List<List<Integer>> levelOrder(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }
    Queue<TreeNode> queue = new LinkedList<>();
    List<List<Integer>> ans = new ArrayList<>();
    //将根节点放入队列,因为要从上到下遍历嘛
    queue.offer(root);
    //队列为空则遍历结束
    while (!queue.isEmpty()) {
        List<Integer> temp = new ArrayList<>();
        //特别注意这个地方,i如果从0开始循环,i<queue.size会动态变化,导致结果错误,这里需要避开这种影响,应该只在第一次循环涉及到size
        for (int i = queue.size(); i > 0; i--) {
            TreeNode treeNode = queue.poll();
            temp.add(treeNode.val);
            if (treeNode.left != null) {
                queue.offer(treeNode.left);
            }
            if (treeNode.right != null) {
                queue.offer(treeNode.right);
            }
        }
        if ((ans.size() % 2) == 1) {
            Collections.reverse(temp);
        }
        ans.add(temp);
    }
    return ans;
}

另一种办法就是采用双端队列(LinkedList实现)

public static List<List<Integer>> levelOrder(TreeNode root) {
    if (root == null) {
        return new ArrayList<>();
    }
    Queue<TreeNode> queue = new LinkedList<>();
    List<List<Integer>> ans = new ArrayList<>();
    //将根节点放入队列,因为要从上到下遍历嘛
    queue.offer(root);
    //队列为空则遍历结束
    while (!queue.isEmpty()) {
        LinkedList<Integer> temp = new LinkedList<>();
        //特别注意这个地方,i如果从0开始循环,i<queue.size会动态变化,导致结果错误,这里需要避开这种影响,应该只在第一次循环涉及到size
        for (int i = queue.size(); i > 0; i--) {
            TreeNode treeNode = queue.poll();
            if (ans.size() % 2 == 1) {
                temp.addFirst(treeNode.val);
            } else {
                temp.addLast(treeNode.val);
            }
            if (treeNode.left != null) {
                queue.offer(treeNode.left);
            }
            if (treeNode.right != null) {
                queue.offer(treeNode.right);
            }
        }
        ans.add(temp);
    }
    return ans;
}

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

描述

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

参考以下这颗二叉搜索树:

     5
    / \
   2   6
  / \
 1   3

示例 1:

输入: [1,6,3,2,5]
输出: false

示例 2:

输入: [1,3,2,6,5]
输出: true

提示:

  • 数组长度 <= 1000

解析

  • 二叉搜索树定义: 左子树中所有节点的值 << 根节点的值;右子树中所有节点的值 >> 根节点的值;其左、右子树也分别为二叉搜索树。

比如下面这棵二叉树,他的后续遍历是[3,5,4,10,12,9]

image.png

我们知道后续遍历的最后一个数字一定是根节点,所以数组中最后一个数字9就是根节点,我们从前往后找到第一个比9大的数字10,那么10后面的[10,12](除了9)都是9的右子节点,10前面的[3,5,4]都是9的左子节点,后面的需要判断一下,如果有小于9的,说明不是二叉搜索树,直接返回false。然后再以递归的方式判断左右子树。

再来看一个,他的后续遍历是[3,5,13,10,12,9]

image.png

我们来根据数组拆分,第一个比9大的后面都是9的右子节点[13,10,12]。然后再拆分这个数组,12是根节点,第一个比12大的后面都是12的右子节点[13,10],但我们看到10是比12小的,他不可能是12的右子节点,所以我们能确定这棵树不是二叉搜索树。搞懂了上面的原理我们再来看下代码。

代码

public boolean verifyPostorder(int[] postorder) {
    return judge(postorder, 0, postorder.length - 1);
}

private boolean judge(int[] postorder, int left, int right) {
    /*
     * 如果left==right,就一个节点不需要判断了,如果left>right说明没有节点,也不用再看了,否则就要继续往下判断
     */
    if (left >= right) {
        return true;
    }
    /*
     * 因为数组中最后一个值postorder[right]是根节点,这里从左往右找出第一个比根节点大的值,
     * 他后面的都是根节点的右子节点(包含当前值,不包含最后一个值,因为最后一个是根节点),他前面的都是根节点的左子节点
     */
    int index = left;
    int root = postorder[right];
    while (postorder[index] < root) {
        index++;
    }
    int temp = index;
    /*
     * 因为postorder[index]前面的值都是比根节点root小的,
     * 我们还需要确定postorder[index]后面的值都要比根节点root大,
     * 如果后面有比根节点小的直接返回false
     */
    while (temp < right) {
        if (postorder[temp++] < root) {
            return false;
        }
    }
    /*
     * 对左右节点递归判断
     */
    return judge(postorder, left, index - 1) && judge(postorder, index, right - 1);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值