剑指 Offer 题解
3. 数组中重复的数字
题目描述
在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
Input:
{2, 3, 1, 0, 2, 5}
Output:
2
解题思路
要求时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。
对于这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素调整到第 i 个位置上进行求解。
以 (2, 3, 1, 0, 2, 5) 为例,遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复:
1 public boolean duplicate(int[] nums, int length, int[] duplication) { 2 if (nums == null || length <= 0) 3 return false; 4 for (int i = 0; i < length; i++) { 5 while (nums[i] != i) { 6 if (nums[i] == nums[nums[i]]) { 7 duplication[0] = nums[i]; 8 return true; 9 } 10 swap(nums, i, nums[i]); 11 } 12 } 13 return false; 14 } 15 16 private void swap(int[] nums, int i, int j) { 17 int t = nums[i]; 18 nums[i] = nums[j]; 19 nums[j] = t; 20 }
4. 二维数组中的查找
题目描述
给定一个二维数组,其每一行从左到右递增排序,从上到下也是递增排序。给定一个数,判断这个数是否在该二维数组中。
Consider the following matrix:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
Given target = 5, return true.
Given target = 20, return false.
解题思路
要求时间复杂度 O(M + N),空间复杂度 O(1)。其中 M 为行数,N 为 列数。
该二维数组中的一个数,小于它的数一定在其左边,大于它的数一定在其下边。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间,当前元素的查找区间为左下角的所有元素。
1 public boolean Find(int target, int[][] matrix) { 2 if (matrix == null || matrix.length == 0 || matrix[0].length == 0) 3 return false; 4 int rows = matrix.length, cols = matrix[0].length; 5 int r = 0, c = cols - 1; // 从右上角开始 6 while (r <= rows - 1 && c >= 0) { 7 if (target == matrix[r][c]) 8 return true; 9 else if (target > matrix[r][c]) 10 r++; 11 else 12 c--; 13 } 14 return false; 15 }
5. 替换空格
题目描述
将一个字符串中的空格替换成 "%20"。
Input:
"A B"
Output:
"A%20B"
解题思路
在字符串尾部填充任意字符,使得字符串的长度等于替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。
令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2 从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。
从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。
1 public String replaceSpace(StringBuffer str) { 2 int P1 = str.length() - 1; 3 for (int i = 0; i <= P1; i++) 4 if (str.charAt(i) == ' ') 5 str.append(" "); 6 7 int P2 = str.length() - 1; 8 while (P1 >= 0 && P2 > P1) { 9 char c = str.charAt(P1--); 10 if (c == ' ') { 11 str.setCharAt(P2--, '0'); 12 str.setCharAt(P2--, '2'); 13 str.setCharAt(P2--, '%'); 14 } else { 15 str.setCharAt(P2--, c); 16 } 17 } 18 return str.toString(); 19 }
6. 从尾到头打印链表
题目描述
从尾到头反过来打印出每个结点的值。
解题思路
使用递归
要逆序打印链表 1->2->3(3,2,1),可以先逆序打印链表 2->3(3,2),最后再打印第一个节点 1。而链表 2->3 可以看成一个新的链表,要逆序打印该链表可以继续使用求解函数,也就是在求解函数中调用自己,这就是递归函数。
1 public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { 2 ArrayList<Integer> ret = new ArrayList<>(); 3 if (listNode != null) { 4 ret.addAll(printListFromTailToHead(listNode.next)); 5 ret.add(listNode.val); 6 } 7 return ret; 8 }
使用头插法
使用头插法可以得到一个逆序的链表。
头结点和第一个节点的区别:
- 头结点是在头插法中使用的一个额外节点,这个节点不存储值;
- 第一个节点就是链表的第一个真正存储值的节点。
1 public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { 2 // 头插法构建逆序链表 3 ListNode head = new ListNode(-1); 4 while (listNode != null) { 5 ListNode memo = listNode.next; 6 listNode.next = head.next; 7 head.next = listNode; 8 listNode = memo; 9 } 10 // 构建 ArrayList 11 ArrayList<Integer> ret = new ArrayList<>(); 12 head = head.next; 13 while (head != null) { 14 ret.add(head.val); 15 head = head.next; 16 } 17 return ret; 18 }
使用栈
栈具有后进先出的特点,在遍历链表时将值按顺序放入栈中,最后出栈的顺序即为逆序。
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) { Stack<Integer> stack = new Stack<>(); while (listNode != null) { stack.add(listNode.val); listNode = listNode.next; } ArrayList<Integer> ret = new ArrayList<>(); while (!stack.isEmpty()) ret.add(stack.pop()); return ret; }
7. 重建二叉树
题目描述
根据二叉树的前序遍历和中序遍历的结果,重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
解题思路
前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。
// 缓存中序遍历数组每个值对应的索引 private Map<Integer, Integer> indexForInOrders = new HashMap<>(); public TreeNode reConstructBinaryTree(int[] pre, int[] in) { for (int i = 0; i < in.length; i++) indexForInOrders.put(in[i], i); return reConstructBinaryTree(pre, 0, pre.length - 1, 0); } private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL) { if (preL > preR) return null; TreeNode root = new TreeNode(pre[preL]); int inIndex = indexForInOrders.get(root.val); int leftTreeSize = inIndex - inL; root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL); root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1); return root; }
8. 二叉树的下一个结点
题目描述
给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。
1 public class TreeLinkNode { 2 3 int val; 4 TreeLinkNode left = null; 5 TreeLinkNode right = null; 6 TreeLinkNode next = null; 7 8 TreeLinkNode(int val) { 9 this.val = val; 10 } 11 }
解题思路
① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点;
② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。
public TreeLinkNode GetNext(TreeLinkNode pNode) { if (pNode.right != null) { TreeLinkNode node = pNode.right; while (node.left != null) node = node.left; return node; } else { while (pNode.next != null) { TreeLinkNode parent = pNode.next; if (parent.left == pNode) return parent; pNode = pNode.next; } } return null; }
9. 用两个栈实现队列
题目描述
用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。
解题思路
in 栈用来处理入栈(push)操作,out 栈用来处理出栈(pop)操作。一个元素进入 in 栈之后,出栈的顺序被反转。当元素要出栈时,需要先进入 out 栈,此时元素出栈顺序再一次被反转,因此出栈顺序就和最开始入栈顺序是相同的,先进入的元素先退出,这就是队列的顺序。
Stack<Integer> in = new Stack<Integer>(); Stack<Integer> out = new Stack<Integer>(); public void push(int node) { in.push(node); } public int pop() throws Exception { if (out.isEmpty()) while (!in.isEmpty()) out.push(in.pop()); if (out.isEmpty()) throw new Exception("queue is empty"); return out.pop(); }
10.1 斐波那契数列
题目描述
求斐波那契数列的第 n 项,n <= 39。
解题思路
如果使用递归求解,会重复计算一些子问题。例如,计算 f(4) 需要计算 f(3) 和 f(2),计算 f(3) 需要计算 f(2) 和 f(1),可以看到 f(2) 被重复计算了。
递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。
1 public int Fibonacci(int n) { 2 if (n <= 1) 3 return n; 4 int[] fib = new int[n + 1]; 5 fib[1] = 1; 6 for (int i = 2; i <= n; i++) 7 fib[i] = fib[i - 1] + fib[i - 2]; 8 return fib[n]; 9 }
考虑到第 i 项只与第 i-1 和第 i-2 项有关,因此只需要存储前两项的值就能求解第 i 项,从而将空间复杂度由 O(N) 降低为 O(1)。
1 public int Fibonacci(int n) { 2 if (n <= 1) 3 return n; 4 int pre2 = 0, pre1 = 1; 5 int fib = 0; 6 for (int i = 2; i <= n; i++) { 7 fib = pre2 + pre1; 8 pre2 = pre1; 9 pre1 = fib; 10 } 11 return fib; 12 }
由于待求解的 n 小于 40,因此可以将前 40 项的结果先进行计算,之后就能以 O(1) 时间复杂度得到第 n 项的值。
1 public class Solution { 2 3 private int[] fib = new int[40]; 4 5 public Solution() { 6 fib[1] = 1; 7 for (int i = 2; i < fib.length; i++) 8 fib[i] = fib[i - 1] + fib[i - 2]; 9 } 10 11 public int Fibonacci(int n) { 12 return fib[n]; 13 } 14 }
10.2 矩形覆盖
题目描述
我们可以用 2*1 的小矩形横着或者竖着去覆盖更大的矩形。请问用 n 个 2*1 的小矩形无重叠地覆盖一个 2*n 的大矩形,总共有多少种方法?
解题思路
当 n 为 1 时,只有一种覆盖方法:
当 n 为 2 时,有两种覆盖方法:
要覆盖 2*n 的大矩形,可以先覆盖 2*1 的矩形,再覆盖 2*(n-1) 的矩形;或者先覆盖 2*2 的矩形,再覆盖 2*(n-2) 的矩形。而覆盖 2*(n-1) 和 2*(n-2) 的矩形可以看成子问题。该问题的递推公式如下:
1 public int RectCover(int n) { 2 if (n <= 2) 3 return n; 4 int pre2 = 1, pre1 = 2; 5 int result = 0; 6 for (int i = 3; i <= n; i++) { 7 result = pre2 + pre1; 8 pre2 = pre1; 9 pre1 = result; 10 } 11 return result; 12 }
10.3 跳台阶
题目描述
一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
解题思路
当 n = 1 时,只有一种跳法:
当 n = 2 时,有两种跳法:
跳 n 阶台阶,可以先跳 1 阶台阶,再跳 n-1 阶台阶;或者先跳 2 阶台阶,再跳 n-2 阶台阶。而 n-1 和 n-2 阶台阶的跳法可以看成子问题,该问题的递推公式为:
1 public int JumpFloor(int n) { 2 if (n <= 2) 3 return n; 4 int pre2 = 1, pre1 = 2; 5 int result = 1; 6 for (int i = 2; i < n; i++) { 7 result = pre2 + pre1; 8 pre2 = pre1; 9 pre1 = result; 10 } 11 return result; 12 }
10.4 变态跳台阶
题目描述
一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级... 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
解题思路
动态规划
1 public int JumpFloorII(int target) { 2 int[] dp = new int[target]; 3 Arrays.fill(dp, 1); 4 for (int i = 1; i < target; i++) 5 for (int j = 0; j < i; j++) 6 dp[i] += dp[j]; 7 return dp[target - 1]; 8 }
数学推导
跳上 n-1 级台阶,可以从 n-2 级跳 1 级上去,也可以从 n-3 级跳 2 级上去...,那么
f(n-1) = f(n-2) + f(n-3) + ... + f(0)
同样,跳上 n 级台阶,可以从 n-1 级跳 1 级上去,也可以从 n-2 级跳 2 级上去... ,那么
f(n) = f(n-1) + f(n-2) + ... + f(0)
综上可得
f(n) - f(n-1) = f(n-1)
即
f(n) = 2*f(n-1)
所以 f(n) 是一个等比数列
public int JumpFloorII(int target) { return (int) Math.pow(2, target - 1); }
11. 旋转数组的最小数字
题目描述
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
解题思路
将旋转数组对半分可以得到一个包含最小元素的新旋转数组,以及一个非递减排序的数组。新的旋转数组的数组元素是原数组的一半,从而将问题规模减少了一半,这种折半性质的算法的时间复杂度为 O(logN)(为了方便,这里将 log2N 写为 logN)。
此时问题的关键在于确定对半分得到的两个数组哪一个是旋转数组,哪一个是非递减数组。我们很容易知道非递减数组的第一个元素一定小于等于最后一个元素。
通过修改二分查找算法进行求解(l 代表 low,m 代表 mid,h 代表 high):
- 当 nums[m] <= nums[h] 时,表示 [m, h] 区间内的数组是非递减数组,[l, m] 区间内的数组是旋转数组,此时令 h = m;
- 否则 [m + 1, h] 区间内的数组是旋转数组,令 l = m + 1。
1 public int minNumberInRotateArray(int[] nums) { 2 if (nums.length == 0) 3 return 0; 4 int l = 0, h = nums.length - 1; 5 while (l < h) { 6 int m = l + (h - l) / 2; 7 if (nums[m] <= nums[h]) 8 h = m; 9 else 10 l = m + 1; 11 } 12 return nums[l]; 13 }
如果数组元素允许重复,会出现一个特殊的情况:nums[l] == nums[m] == nums[h],此时无法确定解在哪个区间,需要切换到顺序查找。例如对于数组 {1,1,1,0,1},l、m 和 h 指向的数都为 1,此时无法知道最小数字 0 在哪个区间。
1 public int minNumberInRotateArray(int[] nums) { 2 if (nums.length == 0) 3 return 0; 4 int l = 0, h = nums.length - 1; 5 while (l < h) { 6 int m = l + (h - l) / 2; 7 if (nums[l] == nums[m] && nums[m] == nums[h]) 8 return minNumber(nums, l, h); 9 else if (nums[m] <= nums[h]) 10 h = m; 11 else 12 l = m + 1; 13 } 14 return nums[l]; 15 } 16 17 private int minNumber(int[] nums, int l, int h) { 18 for (int i = l; i < h; i++) 19 if (nums[i] > nums[i + 1]) 20 return nums[i + 1]; 21 return nums[l]; 22 }
12. 矩阵中的路径
题目描述
判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向上下左右移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。
例如下面的矩阵包含了一条 bfce 路径。
解题思路
使用回溯法(backtracking)进行求解,它是一种暴力搜索方法,通过搜索所有可能的结果来求解问题。回溯法在一次搜索结束时需要进行回溯(回退),将这一次搜索过程中设置的状态进行清除,从而开始一次新的搜索过程。例如下图示例中,从 f 开始,下一步有 4 种搜索可能,如果先搜索 b,需要将 b 标记为已经使用,防止重复使用。在这一次搜索结束之后,需要将 b 的已经使用状态清除,并搜索 c。
本题的输入是数组而不是矩阵(二维数组),因此需要先将数组转换成矩阵。
1 private final static int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; 2 private int rows; 3 private int cols; 4 5 public boolean hasPath(char[] array, int rows, int cols, char[] str) { 6 if (rows == 0 || cols == 0) return false; 7 this.rows = rows; 8 this.cols = cols; 9 boolean[][] marked = new boolean[rows][cols]; 10 char[][] matrix = buildMatrix(array); 11 for (int i = 0; i < rows; i++) 12 for (int j = 0; j < cols; j++) 13 if (backtracking(matrix, str, marked, 0, i, j)) 14 return true; 15 16 return false; 17 } 18 19 private boolean backtracking(char[][] matrix, char[] str, 20 boolean[][] marked, int pathLen, int r, int c) { 21 22 if (pathLen == str.length) return true; 23 if (r < 0 || r >= rows || c < 0 || c >= cols 24 || matrix[r][c] != str[pathLen] || marked[r][c]) { 25 26 return false; 27 } 28 marked[r][c] = true; 29 for (int[] n : next) 30 if (backtracking(matrix, str, marked, pathLen + 1, r + n[0], c + n[1])) 31 return true; 32 marked[r][c] = false; 33 return false; 34 } 35 36 private char[][] buildMatrix(char[] array) { 37 char[][] matrix = new char[rows][cols]; 38 for (int r = 0, idx = 0; r < rows; r++) 39 for (int c = 0; c < cols; c++) 40 matrix[r][c] = array[idx++]; 41 return matrix; 42 }
13. 机器人的运动范围
题目描述
地上有一个 m 行和 n 列的方格。一个机器人从坐标 (0, 0) 的格子开始移动,每一次只能向左右上下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 k 的格子。
例如,当 k 为 18 时,机器人能够进入方格 (35,37),因为 3+5+3+7=18。但是,它不能进入方格 (35,38),因为 3+5+3+8=19。请问该机器人能够达到多少个格子?
解题思路
使用深度优先搜索(Depth First Search,DFS)方法进行求解。回溯是深度优先搜索的一种特例,它在一次搜索过程中需要设置一些本次搜索过程的局部状态,并在本次搜索结束之后清除状态。而普通的深度优先搜索并不需要使用这些局部状态,虽然还是有可能设置一些全局状态。
1 private static final int[][] next = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; 2 private int cnt = 0; 3 private int rows; 4 private int cols; 5 private int threshold; 6 private int[][] digitSum; 7 8 public int movingCount(int threshold, int rows, int cols) { 9 this.rows = rows; 10 this.cols = cols; 11 this.threshold = threshold; 12 initDigitSum(); 13 boolean[][] marked = new boolean[rows][cols]; 14 dfs(marked, 0, 0); 15 return cnt; 16 } 17 18 private void dfs(boolean[][] marked, int r, int c) { 19 if (r < 0 || r >= rows || c < 0 || c >= cols || marked[r][c]) 20 return; 21 marked[r][c] = true; 22 if (this.digitSum[r][c] > this.threshold) 23 return; 24 cnt++; 25 for (int[] n : next) 26 dfs(marked, r + n[0], c + n[1]); 27 } 28 29 private void initDigitSum() { 30 int[] digitSumOne = new int[Math.max(rows, cols)]; 31 for (int i = 0; i < digitSumOne.length; i++) { 32 int n = i; 33 while (n > 0) { 34 digitSumOne[i] += n % 10; 35 n /= 10; 36 } 37 } 38 this.digitSum = new int[rows][cols]; 39 for (int i = 0; i < this.rows; i++) 40 for (int j = 0; j < this.cols; j++) 41 this.digitSum[i][j] = digitSumOne[i] + digitSumOne[j]; 42 }
14. 剪绳子
题目描述
把一根绳子剪成多段,并且使得每段的长度乘积最大。
n = 2
return 1 (2 = 1 + 1)
n = 10
return 36 (10 = 3 + 3 + 4)
解题思路
贪心
尽可能多剪长度为 3 的绳子,并且不允许有长度为 1 的绳子出现。如果出现了,就从已经切好长度为 3 的绳子中拿出一段与长度为 1 的绳子重新组合,把它们切成两段长度为 2 的绳子。
证明:当 n >= 5 时,3(n - 3) - n = 2n - 9 > 0,且 2(n - 2) - n = n - 4 > 0。因此在 n >= 5 的情况下,将绳子剪成一段为 2 或者 3,得到的乘积会更大。又因为 3(n - 3) - 2(n - 2) = n - 5 >= 0,所以剪成一段长度为 3 比长度为 2 得到的乘积更大。
1 public int integerBreak(int n) { 2 if (n < 2) 3 return 0; 4 if (n == 2) 5 return 1; 6 if (n == 3) 7 return 2; 8 int timesOf3 = n / 3; 9 if (n - timesOf3 * 3 == 1) 10 timesOf3--; 11 int timesOf2 = (n - timesOf3 * 3) / 2; 12 return (int) (Math.pow(3, timesOf3)) * (int) (Math.pow(2, timesOf2)); 13 }
动态规划
1 public int integerBreak(int n) { 2 int[] dp = new int[n + 1]; 3 dp[1] = 1; 4 for (int i = 2; i <= n; i++) 5 for (int j = 1; j < i; j++) 6 dp[i] = Math.max(dp[i], Math.max(j * (i - j), dp[j] * (i - j))); 7 return dp[n]; 8 }
15. 二进制中 1 的个数
题目描述
输入一个整数,输出该数二进制表示中 1 的个数。
n&(n-1)
该位运算去除 n 的位级表示中最低的那一位。
n : 10110100
n-1 : 10110011
n&(n-1) : 10110000
时间复杂度:O(M),其中 M 表示 1 的个数。
1 public int NumberOf1(int n) { 2 int cnt = 0; 3 while (n != 0) { 4 cnt++; 5 n &= (n - 1); 6 } 7 return cnt; 8 }
Integer.bitCount()
public int NumberOf1(int n) { return Integer.bitCount(n); }
16. 数值的整数次方
题目描述
给定一个 double 类型的浮点数 base 和 int 类型的整数 exponent,求 base 的 exponent 次方。
解题思路
下面的讨论中 x 代表 base,n 代表 exponent。
因为 (x*x)n/2 可以通过递归求解,并且每次递归 n 都减小一半,因此整个算法的时间复杂度为 O(logN)。
public double Power(double base, int exponent) { if (exponent == 0) return 1; if (exponent == 1) return base; boolean isNegative = false; if (exponent < 0) { exponent = -exponent; isNegative = true; } double pow = Power(base * base, exponent / 2); if (exponent % 2 != 0) pow = pow * base; return isNegative ? 1 / pow : pow; }
17. 打印从 1 到最大的 n 位数
题目描述
输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。
解题思路
由于 n 可能会非常大,因此不能直接用 int 表示数字,而是用 char 数组进行存储。
使用回溯法得到所有的数。
public void print1ToMaxOfNDigits(int n) { if (n <= 0) return; char[] number = new char[n]; print1ToMaxOfNDigits(number, 0); } private void print1ToMaxOfNDigits(char[] number, int digit) { if (digit == number.length) { printNumber(number); return; } for (int i = 0; i < 10; i++) { number[digit] = (char) (i + '0'); print1ToMaxOfNDigits(number, digit + 1); } } private void printNumber(char[] number) { int index = 0; while (index < number.length && number[index] == '0') index++; while (index < number.length) System.out.print(number[index++]); System.out.println(); }
18.1 在 O(1) 时间内删除链表节点
解题思路
① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 O(1)。
② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。
综上,如果进行 N 次操作,那么大约需要操作节点的次数为 N-1+N=2N-1,其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数,N 表示 1 个尾节点以 O(N) 的时间复杂度操作节点的总次数。(2N-1)/N ~ 2,因此该算法的平均时间复杂度为 O(1)。
public ListNode deleteNode(ListNode head, ListNode tobeDelete) { if (head == null || tobeDelete == null) return null; if (tobeDelete.next != null) { // 要删除的节点不是尾节点 ListNode next = tobeDelete.next; tobeDelete.val = next.val; tobeDelete.next = next.next; } else { if (head == tobeDelete) // 只有一个节点 head = null; else { ListNode cur = head; while (cur.next != tobeDelete) cur = cur.next; cur.next = null; } } return head; }
18.2 删除链表中重复的结点
题目描述
解题描述
1 public ListNode deleteDuplication(ListNode pHead) { 2 if (pHead == null || pHead.next == null) 3 return pHead; 4 ListNode next = pHead.next; 5 if (pHead.val == next.val) { 6 while (next != null && pHead.val == next.val) 7 next = next.next; 8 return deleteDuplication(next); 9 } else { 10 pHead.next = deleteDuplication(pHead.next); 11 return pHead; 12 } 13 }
19. 正则表达式匹配
题目描述
请实现一个函数用来匹配包括 '.' 和 '*' 的正则表达式。模式中的字符 '.' 表示任意一个字符,而 '*' 表示它前面的字符可以出现任意次(包含 0 次)。
在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串 "aaa" 与模式 "a.a" 和 "ab*ac*a" 匹配,但是与 "aa.a" 和 "ab*a" 均不匹配。
解题思路
应该注意到,'.' 是用来当做一个任意字符,而 '*' 是用来重复前面的字符。这两个的作用不同,不能把 '.' 的作用和 '*' 进行类比,从而把它当成重复前面字符一次。
1 public boolean match(char[] str, char[] pattern) { 2 3 int m = str.length, n = pattern.length; 4 boolean[][] dp = new boolean[m + 1][n + 1]; 5 6 dp[0][0] = true; 7 for (int i = 1; i <= n; i++) 8 if (pattern[i - 1] == '*') 9 dp[0][i] = dp[0][i - 2]; 10 11 for (int i = 1; i <= m; i++) 12 for (int j = 1; j <= n; j++) 13 if (str[i - 1] == pattern[j - 1] || pattern[j - 1] == '.') 14 dp[i][j] = dp[i - 1][j - 1]; 15 else if (pattern[j - 1] == '*') 16 if (pattern[j - 2] == str[i - 1] || pattern[j - 2] == '.') { 17 dp[i][j] |= dp[i][j - 1]; // a* counts as single a 18 dp[i][j] |= dp[i - 1][j]; // a* counts as multiple a 19 dp[i][j] |= dp[i][j - 2]; // a* counts as empty 20 } else 21 dp[i][j] = dp[i][j - 2]; // a* only counts as empty 22 23 return dp[m][n]; 24 }






























浙公网安备 33010602011771号