剑指 Offer 35. 复杂链表的复制
难度中等226
请实现 copyRandomList
函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next
指针指向下一个节点,还有一个 random
指针指向链表中的任意节点或者 null
。
示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例 2:
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例 3:
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
示例 4:
输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。
public Node copyRandomList(Node head) {
if(head == null) return null;
Node cur = head;
Map<Node, Node> map = new HashMap<>();
// 3. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
while(cur != null) {
map.put(cur, new Node(cur.val));
cur = cur.next;
}
cur = head;
// 4. 构建新链表的 next 和 random 指向
while(cur != null) {
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
// 5. 返回新链表的头节点
return map.get(head);
}
剑指 Offer 37. 序列化二叉树
难度困难166
请实现两个函数,分别用来序列化和反序列化二叉树。
示例:
你可以将以下二叉树:
1
/ \
2 3
/ \
4 5
序列化为 "[1,2,3,null,null,4,5]"
public class Codec {
public String rserialize(TreeNode root, String str) {
if (root == null) {
str += "None,";
} else {
str += str.valueOf(root.val) + ",";
str = rserialize(root.left, str);
str = rserialize(root.right, str);
}
return str;
}
public String serialize(TreeNode root) {
return rserialize(root, "");
}
public TreeNode rdeserialize(List<String> l) {
if (l.get(0).equals("None")) {
l.remove(0);
return null;
}
TreeNode root = new TreeNode(Integer.valueOf(l.get(0)));
l.remove(0);
root.left = rdeserialize(l);
root.right = rdeserialize(l);
return root;
}
public TreeNode deserialize(String data) {
String[] data_array = data.split(",");
List<String> data_list = new LinkedList<String>(Arrays.asList(data_array));
return rdeserialize(data_list);
}
}
//下面为法二,思路类似
public class Codec {
public String serialize(TreeNode root) {
if(root == null) return "[]";
StringBuilder res = new StringBuilder("[");
Queue<TreeNode> queue = new LinkedList<>() {{ add(root); }};
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(node != null) {
res.append(node.val + ",");
queue.add(node.left);
queue.add(node.right);
}
else res.append("null,");
}
res.deleteCharAt(res.length() - 1);
res.append("]");
return res.toString();
}
public TreeNode deserialize(String data) {
if(data.equals("[]")) return null;
String[] vals = data.substring(1, data.length() - 1).split(",");
TreeNode root = new TreeNode(Integer.parseInt(vals[0]));
Queue<TreeNode> queue = new LinkedList<>() {{ add(root); }};
int i = 1;
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(!vals[i].equals("null")) {
node.left = new TreeNode(Integer.parseInt(vals[i]));
queue.add(node.left);
}
i++;
if(!vals[i].equals("null")) {
node.right = new TreeNode(Integer.parseInt(vals[i]));
queue.add(node.right);
}
i++;
}
return root;
}
}
提示:
-10000 <= Node.val <= 10000
Node.random
为空(null)或指向链表中的节点。- 节点数目不超过 1000 。
方法一:哈希表
利用哈希表的查询特点,考虑构建 原链表节点 和 新链表对应节点 的键值对映射关系,再遍历构建新链表各节点的 next 和 random 引用指向即可。
算法流程:
若头节点 head 为空节点,直接返回 null ;
初始化: 哈希表 dic , 节点 cur 指向头节点;
复制链表:
建立新节点,并向 dic 添加键值对 (原 cur 节点, 新 cur 节点) ;
cur 遍历至原链表下一节点;
构建新链表的引用指向:
构建新节点的 next 和 random 引用指向;
cur 遍历至原链表下一节点;
返回值: 新链表的头节点 dic[cur] ;
class Solution {
public Node copyRandomList(Node head) {
if(head == null) return null;
Node cur = head;
Map<Node, Node> map = new HashMap<>();
// 3. 复制各节点,并建立 “原节点 -> 新节点” 的 Map 映射
while(cur != null) {
map.put(cur, new Node(cur.val));
//这个是复制链表的关键;
cur = cur.next;
}
cur = head;
// 4. 构建新链表的 next 和 random 指向
while(cur != null) {
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
// 5. 返回新链表的头节点
return map.get(head);
}
}
剑指 Offer 36. 二叉搜索树与双向链表
难度中等255收藏分享切换为英文接收动态反馈
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了让您更好地理解问题,以下面的二叉搜索树为例:
我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。
解题思路:
本文解法基于性质:二叉搜索树的中序遍历为 递增序列 。
将 二叉搜索树 转换成一个 “排序的循环双向链表” ,其中包含三个要素:
- 排序链表: 节点应从小到大排序,因此应使用 中序遍历 “从小到大”访问树的节点。
- 双向链表: 在构建相邻节点的引用关系时,设前驱节点
pre
和当前节点cur
,不仅应构建pre.right = cur
,也应构建cur.left = pre
。 - 循环链表: 设链表头节点
head
和尾节点tail
,则应构建head.left = tail
和tail.right = head
。
中序遍历 为对二叉树作 “左、根、右” 顺序遍历,递归实现如下:
// 打印中序遍历
void dfs(Node root) {
if(root == null) return;
dfs(root.left); // 左
System.out.println(root.val); // 根
dfs(root.right); // 右
}
根据以上分析,考虑使用中序遍历访问树的各节点 cur
;并在访问每个节点时构建 cur
和前驱节点 pre
的引用指向;中序遍历完成后,最后构建头节点和尾节点的引用指向即可。
算法流程:
dfs(cur):
递归法中序遍历;
- 终止条件: 当节点
cur
为空,代表越过叶节点,直接返回; - 递归左子树,即
dfs(cur.left)
; - 构建链表:
- 当
pre
为空时: 代表正在访问链表头节点,记为head
; - 当
pre
不为空时: 修改双向节点引用,即pre.right = cur
,cur.left = pre
; - 保存
cur
: 更新pre = cur
,即节点cur
是后继节点的pre
;
- 当
- 递归右子树,即
dfs(cur.right)
;
treeToDoublyList(root):
- 特例处理: 若节点
root
为空,则直接返回; - 初始化: 空节点
pre
; - 转化为双向链表: 调用
dfs(root)
; - 构建循环链表: 中序遍历完成后,
head
指向头节点,pre
指向尾节点,因此修改head
和pre
的双向节点引用即可; - 返回值: 返回链表的头节点
head
即可;
class Solution {
Node pre, head;
public Node treeToDoublyList(Node root) {
if(root == null) return null;
dfs(root);
head.left = pre;
pre.right = head;
return head;
}
void dfs(Node cur) {
if(cur == null) return;
dfs(cur.left);
if(pre != null) pre.right = cur;
else head = cur;
cur.left = pre;
pre = cur;
dfs(cur.right);
}
}
剑指 Offer 38. 字符串的排列
难度中等343
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]
解题思路:
对于一个长度为 n 的字符串(假设字符互不重复),其排列方案数共有:
n×(n−1)×(n−2)…×2×1
排列方案的生成:
根据字符串排列的特点,考虑深度优先搜索所有排列方案。即通过字符交换,先固定第 1 位字符( n 种情况)、再固定第 2 位字符( n-1 种情况)、… 、最后固定第 n 位字符( 1 种情况)。
重复排列方案与剪枝:
当字符串存在重复字符时,排列方案中也存在重复的排列方案。为排除重复方案,需在固定某位字符时,保证 “每种字符只在此位固定一次” ,即遇到重复字符时不交换,直接跳过。从 DFS 角度看,此操作称为 “剪枝” 。
递归解析:
终止条件: 当 x = len ( c ) - 1 时,代表所有位已固定(最后一位只有 1 种情况),则将当前组合 c 转化为字符串并加入 res ,并返回;
递推参数: 当前固定位 x ;
递推工作: 初始化一个 Set ,用于排除重复的字符;将第 x 位字符与 i∈ [x, len ( c )] 字符分别交换,并进入下层递归;
剪枝: 若 c[i] 在 Set 中,代表其是重复字符,因此 “剪枝” ;
将 c[i] 加入 Set ,以便之后遇到重复字符时剪枝;
固定字符: 将字符 c[i] 和 c[x] 交换,即固定 c[i] 为当前位字符;
开启下层递归: 调用 dfs(x + 1) ,即开始固定第 x + 1 个字符;
还原交换: 将字符 c[i] 和 c[x] 交换(还原之前的交换);
class Solution {
List<String> res = new LinkedList<>();
char[] c;
public String[] permutation(String s) {
c = s.toCharArray();
dfs(0);
return res.toArray(new String[res.size()]);
}
void dfs(int x) {
if(x == c.length - 1) {
res.add(String.valueOf(c)); // 添加排列方案
return;
}
HashSet<Character> set = new HashSet<>();
for(int i = x; i < c.length; i++) {
if(set.contains(c[i])) continue; // 重复,因此剪枝
set.add(c[i]);
swap(i, x); // 交换,将 c[i] 固定在第 x 位
dfs(x + 1); // 开启固定第 x + 1 位字符
swap(i, x); // 恢复交换
}
}
void swap(int a, int b) {
char tmp = c[a];
c[a] = c[b];
c[b] = tmp;
}
}
剑指 Offer 39. 数组中出现次数超过一半的数字
难度简单166
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 1:
输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2
限制:
1 <= 数组长度 <= 50000
class Solution {
public int majorityElement(int[] nums) {
int x = 0, votes = 0;
for(int num : nums){
if(votes == 0) x = num;
votes += num == x ? 1 : -1;
}
return x;
}
}
本题常见的三种解法:
- 哈希表统计法: 遍历数组
nums
,用 HashMap 统计各数字的数量,即可找出 众数 。此方法时间和空间复杂度均为 O(N)O(N) 。 - 数组排序法: 将数组
nums
排序,数组中点的元素 一定为众数。 - 摩尔投票法: 核心理念为 票数正负抵消 。此方法时间和空间复杂度分别为 O(N)O(N) 和 O(1)O(1) ,为本题的最佳解法。
摩尔投票法:
设输入数组
nums
的众数为 x ,数组长度为 n 。
推论一: 若记 众数 的票数为+1 ,非众数 的票数为 -1−1 ,则一定有所有数字的 票数和 >0 。
推论二: 若数组的前 a个数字的 票数和=0 ,则 数组剩余 (n−a) 个数字的 票数和一定仍 >0 ,即后(n−a) 个数字的 众数仍为 x
根据以上推论,记数组首个元素为 n1 ,众数为 xx ,遍历并统计票数。当发生 票数和=0 时,剩余数组的众数一定不变 ,这是由于:
- 当 n*1=*x* : 抵消的所有数字中,有一半是众数x* 。
- 当n*1≠*x* : 抵消的所有数字中,众数 x* 的数量为一半或 0 个。
利用此特性,每轮假设发生 票数和=0 都可以 缩小剩余数组区间 。当遍历完成时,最后一轮假设的数字即为众数。
算法流程:
-
初始化: 票数统计
votes = 0
, 众数x
; -
循环:
遍历数组nums中的每个数字 num
- 当 票数
votes
等于 0 ,则假设当前数字num
是众数; - 当
num = x
时,票数votes
自增 1 ;当num != x
时,票数votes
自减 1 ;
- 当 票数
-
返回值: 返回
x
即可;
剑指 Offer 40. 最小的k个数
难度简单262收藏分享切换为英文接收动态反馈
输入整数数组 arr
,找出其中最小的 k
个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
限制:
0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
int[] vec = new int[k];
if (k == 0) { // 排除 0 的情况
return vec;
}
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>() {
public int compare(Integer num1, Integer num2) {
return num2 - num1;
}
});
for (int i = 0; i < k; ++i) {
queue.offer(arr[i]);
}
for (int i = k; i < arr.length; ++i) {
if (queue.peek() > arr[i]) {
queue.poll();
queue.offer(arr[i]);
}
}
for (int i = 0; i < k; ++i) {
vec[i] = queue.poll();
}
return vec;
}
}
剑指 Offer 42. 连续子数组的最大和
难度简单291
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
示例1:
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
提示:
1 <= arr.length <= 10^5
-100 <= arr[i] <= 100
暴力法:
// 时间复杂度:O(n^3)
class Solution {
public int maxSubArray(int[] nums) {
int max = Integer.MIN_VALUE;
for(int i = 0;i < nums.length;i++){
for(int j = i;j < nums.length;j++){
// 计算sum(i,j)
int sum = 0;
for(int k = i;k<j;k++)
sum+=nums[k];
if(sum > max)
max = sum;
}
}
return max;
}
}
// 时间复杂度:O(n^2)
class Solution {
public int maxSubArray(int[] nums) {
int max = Integer.MIN_VALUE;
for(int i = 0;i < nums.length;i++){
int sum = 0;
for(int j = i;j < nums.length;j++){
//sum(i,j)=sum(i,j-1)+nums[j]
sum += nums[j];
if(sum > max)
max = sum;
}
}
return max;
}
}
动态规划
动态规划是本题的最优解法,以下按照标准流程解题。
动态规划解析:
- 状态定义: 设动态规划列表 dp,dp[i] 代表以元素 nums[i] 为结尾的连续子数组最大和。
- 为何定义最大和 dp[i] 中必须包含元素 nums[i]:保证 dp[i]递推到 dp[i+1] 的正确性;如果不包含 nums[i],递推时则不满足题目的 连续子数组 要求。
- 转移方程: 若 dp[i-1] ≤0 ,说明 dp[i - 1]对 dp[i]产生负贡献,即 dp[i-1] + nums[i] 还不如 nums[i]本身大。
- 当 dp[i - 1] > 0 时:执行 dp[i] = dp[i-1] + nums[i] ;
- 当 dp[i - 1] ≤0 时:执行 dp[i] = nums[i] ;
- 初始状态: dp[0] = nums[0],即以 nums[0]结尾的连续子数组最大和为 nums[0]。
- 返回值: 返回 dp列表中的最大值,代表全局最大值。
空间复杂度降低:
- 由于 dp[i]只与 dp[i-1] 和 nums[i] 有关系,因此可以将原数组 nums用作 dp列表,即直接在 nums上修改即可。
- 由于省去 dp列表使用的额外空间,因此空间复杂度从 O(N)降至 O(1)。
复杂度分析:
- 时间复杂度 O(N): 线性遍历数组 nums即可获得结果,使用 O(N) 时间。
- 空间复杂度 O(1): 使用常数大小的额外空间。
class Solution {
public int maxSubArray(int[] nums) {
int res = nums[0];
for(int i = 1; i < nums.length; i++) {
nums[i] += Math.max(nums[i - 1], 0);
res = Math.max(res, nums[i]);
}
return res;
}
}
如果不改变num数组
class Solution {
public int maxSubArray(int[] nums) {
int max = nums[0];
int former = 0;//用于记录dp[i-1]的值,对于dp[0]而言,其前面的dp[-1]=0
int cur = nums[0];//用于记录dp[i]的值
for(int num:nums){
cur = num;
if(former>0) cur +=former;
if(cur>max) max = cur;
former=cur;
}
return max;
}
}
剑指 Offer 46. 把数字翻译成字符串
难度中等238
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
示例 1:
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
解题思路:
根据题意,可按照下图的思路,总结出 “递推公式” (即转移方程)。
因此,此题可用动态规划解决,以下按照流程解题。
class Solution {
public int translateNum(int num) {
String s = String.valueOf(num);
int a = 1, b = 1;
for(int i = 2; i <= s.length(); i++) {
String tmp = s.substring(i - 2, i);
int c = tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0 ? a + b : a;
b = a;
a = c;
}
return a;
}
}
class Solution {
public int translateNum(int num) {
String s = String.valueOf(num);
int[] dp = new int[s.length()+1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= s.length(); i ++){
String temp = s.substring(i-2, i);
if(temp.compareTo("10") >= 0 && temp.compareTo("25") <= 0)
dp[i] = dp[i-1] + dp[i-2];
else
dp[i] = dp[i-1];
}
return dp[s.length()];
}
}
剑指 Offer 47. 礼物的最大价值
难度中等150
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例 1:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
提示:
0 < grid.length <= 200
0 < grid[0].length <= 200
解题思路:
题目说明:从棋盘的左上角开始拿格子里的礼物,并每次 向右 或者 向下 移动一格、直到到达棋盘的右下角。
根据题目说明,易得某单元格只可能从上边单元格或左边单元格到达。
设 f(i, j)为从棋盘左上角走至单元格 (i ,j)的礼物最大累计价值,易得到以下递推关系:f(i,j)等于 f(i,j-1) 和 f(i-1,j)*f中的较大值加上当前单元格礼物价值 grid(i,j) 。
- 状态定义: 设动态规划矩阵 dp ,dp(i,j) 代表从棋盘的左上角开始,到达单元格 (i,j) 时能拿到礼物的最大累计价值。
- 转移方程:
- 当 i = 0 且 j = 0时,为起始元素;
- 当 i*=0 且* j≠0 时,为矩阵第一行元素,只可从左边到达;
- 当 i≠0 且 j*=0 时,为矩阵第一列元素,只可从上边到达;
- 当 i , j ≠0 时,可从左边或上边到达;
class Solution {
public int maxValue(int[][] grid) {
int m = grid.length, n = grid[0].length;
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(i == 0 && j == 0) continue;
if(i == 0) grid[i][j] += grid[i][j - 1] ;
else if(j == 0) grid[i][j] += grid[i - 1][j];
else grid[i][j] += Math.max(grid[i][j - 1], grid[i - 1][j]);
}
}
return grid[m - 1][n - 1];
}
}
class Solution {
public int maxValue(int[][] grid) {
int m = grid.length, n = grid[0].length;
for(int j = 1; j < n; j++) // 初始化第一行
grid[0][j] += grid[0][j - 1];
for(int i = 1; i < m; i++) // 初始化第一列
grid[i][0] += grid[i - 1][0];
for(int i = 1; i < m; i++)
for(int j = 1; j < n; j++)
grid[i][j] += Math.max(grid[i][j - 1], grid[i - 1][j]);
return grid[m - 1][n - 1];
}
}
剑指 Offer 48. 最长不含重复字符的子字符串
难度中等223
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
s.length <= 40000
动态规划解析:
- 状态定义: 设动态规划列表 dp,dp[j] 代表以字符 s[j] 为结尾的 “最长不重复子字符串” 的长度。
- 转移方程: 固定右边界 j ,设字符 s*[j] 左边距离最近的相同字符为 s[i]*,即 s[i] = s[j] 。
-
当 i < 0,即 s[j]左边无相同字符,则 dp[j] = dp[j-1] + 1 ;
-
当 dp[j - 1] < j - i ,说明字符 s[i]在子字符串 dp[j-1]区间之外 ,则 dp[j] = dp[j - 1] + 1;
-
当 dp[j - 1] ≥j−i ,说明字符 s[i] 在子字符串 dp[j-1] 区间之中 ,则 dp[j]的左边界由 s[i] 决定,即 dp[j] = j - i ;
- 返回值: \max(dp),即全局的 “最长不重复子字符串” 的长度。
空间复杂度优化:
由于返回值是取 dp 列表最大值,因此可借助变量 tmp 存储 dp[j] ,变量 res 每轮更新最大值即可。
此优化可节省 dp 列表使用的 O(N)大小的额外空间。
观察转移方程,可知问题为:每轮遍历字符 s[j] 时,如何计算索引 i ?
以下介绍 哈希表 , 线性遍历 两种方法。
方法一:动态规划 + 哈希表
哈希表统计: 遍历字符串 s 时,使用哈希表(记为 dic )统计 各字符最后一次出现的索引位置 。
左边界 i 获取方式: 遍历到 s[j] 时,可通过访问哈希表 dic[s[j]]获取最近的相同字符的索引 i 。
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> dic = new HashMap<>();
int res = 0, tmp = 0;
for(int j = 0; j < s.length(); j++) {
int i = dic.getOrDefault(s.charAt(j), -1); // 获取索引 i
dic.put(s.charAt(j), j); // 更新哈希表
tmp = tmp < j - i ? tmp + 1 : j - i; // dp[j - 1] -> dp[j]
res = Math.max(res, tmp); // max(dp[j - 1], dp[j])
}
return res;
}
}
方法二: 动态规划 + 线性遍历
左边界 i 获取方式: 遍历到 s[j] 时,初始化索引 i = j - 1,向左遍历搜索第一个满足 s[i] = s[j]的字符即可 。
复杂度分析:
时间复杂度 O(N^2) : 其中 N 为字符串长度,动态规划需遍历计算 dp 列表,占用 O(N) ;每轮计算 dp[j]时搜索 i 需要遍历 j 个字符,占用 O(N) 。
空间复杂度 O(1): 几个变量使用常数大小的额外空间。
class Solution {
public int lengthOfLongestSubstring(String s) {
int res = 0, tmp = 0;
for(int j = 0; j < s.length(); j++) {
int i = j - 1;
while(i >= 0 && s.charAt(i) != s.charAt(j)) i--; // 线性查找 i
tmp = tmp < j - i ? tmp + 1 : j - i; // dp[j - 1] -> dp[j]
res = Math.max(res, tmp); // max(dp[j - 1], dp[j])
}
return res;
}
}
方法三:双指针 + 哈希表
本质上与方法一类似,不同点在于左边界 i 的定义。
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> dic = new HashMap<>();
int i = -1, res = 0;
for(int j = 0; j < s.length(); j++) {
if(dic.containsKey(s.charAt(j)))
**i = Math.max(i, dic.get(s.charAt(j))); // 更新左指针重要的一行代码,精辟 i**
dic.put(s.charAt(j), j); // 哈希表记录
res = Math.max(res, j - i); // 更新结果
}
return res;
}
}
剑指 Offer 49. 丑数
难度中等178
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
示例:
输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
说明:
1
是丑数。n
不超过1690。
方法一:最小堆
在排除重复元素的情况下,第 n次从最小堆中取出的元素即为第 n个丑数。
class Solution {
public int nthUglyNumber(int n) {
int[] factors = {2, 3, 5};
Set<Long> seen = new HashSet<Long>();
PriorityQueue<Long> heap = new PriorityQueue<Long>();
seen.add(1L);
heap.offer(1L);
int ugly = 0;
for (int i = 0; i < n; i++) {
long curr = heap.poll();
ugly = (int) curr;
for (int factor : factors) {
long next = curr * factor;
if (seen.add(next)) {
heap.offer(next);
}
}
}
return ugly;
}
}
方法二:动态规划
方法一使用最小堆,会预先存储较多的丑数,导致空间复杂度较高,维护最小堆的过程也导致时间复杂度较高。可以使用动态规划的方法进行优化。
class Solution {
public int nthUglyNumber(int n) {
int[] dp = new int[n + 1];
dp[1] = 1;
int p2 = 1, p3 = 1, p5 = 1;
for (int i = 2; i <= n; i++) {
int num2 = dp[p2] * 2, num3 = dp[p3] * 3, num5 = dp[p5] * 5;
dp[i] = Math.min(Math.min(num2, num3), num5);
if (dp[i] == num2) {
p2++;
}
if (dp[i] == num3) {
p3++;
}
if (dp[i] == num5) {
p5++;
}
}
return dp[n];
}
}
剑指 Offer 50. 第一个只出现一次的字符
难度简单107
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
示例:
s = "abaccdeff"
返回 "b"
s = ""
返回 " "
限制:
0 <= s 的长度 <= 50000
class Solution {
public char firstUniqChar(String s) {
HashMap<Character, Boolean> dic = new HashMap<>();
char[] sc = s.toCharArray();
for(char c : sc)
dic.put(c, !dic.containsKey(c));
for(char c : sc)
if(dic.get(c)) return c;
return ' ';
}
}
方法二:有序哈希表
在哈希表的基础上,有序哈希表中的键值对是 按照插入顺序排序 的。基于此,可通过遍历有序哈希表,实现搜索首个 “数量为 1 的字符”。
哈希表是 去重 的,即哈希表中键值对数量 ≤ 字符串 s
的长度。因此,相比于方法一,方法二减少了第二轮遍历的循环次数。当字符串很长(重复字符很多)时,方法二则效率更高。
Java 使用 LinkedHashMap
实现有序哈希表。
class Solution {
public char firstUniqChar(String s) {
Map<Character, Boolean> dic = new LinkedHashMap<>();
char[] sc = s.toCharArray();
for(char c : sc)
dic.put(c, !dic.containsKey(c));
for(Map.Entry<Character, Boolean> d : dic.entrySet()){
if(d.getValue()) return d.getKey();
}
return ' ';
}
}
剑指 Offer 52. 两个链表的第一个公共节点
难度简单237
输入两个链表,找出它们的第一个公共节点。
如下面的两个链表**:**
在节点 c1 开始相交。
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
我们使用两个指针 node1
,node2
分别指向两个链表 headA
,headB
的头结点,然后同时分别逐结点遍历,当 node1
到达链表 headA
的末尾时,重新定位到链表 headB
的头结点;当 node2
到达链表 headB
的末尾时,重新定位到链表 headA
的头结点。
这样,当它们相遇时,所指向的结点就是第一个公共结点。
太6了,我的理解: 两个链表长度分别为L1+C、L2+C, C为公共部分的长度,按照楼主的做法: 第一个人走了L1+C步后,回到第二个人起点走L2步;第2个人走了L2+C步后,回到第一个人起点走L1步。 当两个人走的步数都为L1+L2+C时就两个家伙就相爱了
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode A = headA, B = headB;
while (A != B) {
A = A != null ? A.next : headB;
B = B != null ? B.next : headA;
}
return A;
}
}
这些代码来源于力扣官网和一些大神的解答,如若侵权,本人定删。