链表
1 两数相加
题目描述
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) 输出:7 -> 0 -> 8 原因:342 + 465 = 807
解法1 初等数学
新创建一个链表head
,遍历两个非空链表,设置一个进位值carry
,初始值为0,把链表值与进位值相加取余,即为新链表的值,同时更新进位值。
实现代码:
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = new ListNode(0);
ListNode p = l1, q = l2, cur = head;
int sum = 0, carry = 0;
while (p != null || q != null) {
int x = (p != null) ? p.val : 0;
int y = (q != null) ? q.val : 0;
sum = x + y + carry;
cur.next = new ListNode(sum % 10);
cur = cur.next;
carry = sum / 10;
if (p != null) p = p.next;
if (q != null) q = q.next;
}
if (carry > 0) {
cur.next = new ListNode(carry);
}
return head.next;
}
复杂度分析
时间复杂度 | O(max(m,n)) |
---|---|
空间复杂度 | O(max(m,n)) |
146. LRU缓存机制
题目描述
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。
获取数据 get(key) - 如果关键字 (key) 存在于缓存中,则获取关键字的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字/值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。LRUCache cache = new LRUCache( 2 /* 缓存容量 */ ); cache.put(1, 1); cache.put(2, 2); cache.get(1); // 返回 1 cache.put(3, 3); // 该操作会使得关键字 2 作废 cache.get(2); // 返回 -1 (未找到) cache.put(4, 4); // 该操作会使得关键字 1 作废 cache.get(1); // 返回 -1 (未找到) cache.get(3); // 返回 3 cache.get(4); // 返回 4
解法1 双向链表+哈希表
一、 LRU 算法
是一种缓存淘汰策略。计算机的缓存容量有限,如果缓存满了就要删除一些内容,给新内容腾位置。LRU 缓存淘汰算法就是一种常用策略。LRU 的全称是 Least Recently Used,也就是说我们认为最近使用过的数据应该是有用的,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。
就比如手机允许同时开启3个应用程序,当我想要开启第四个的时候,就必须关闭一个腾出位置,此时就优先关闭最久未使用的应用程序。
LRU 算法实际上是设计数据结构:首先要接收一个 capacity 参数作为缓存的最大容量,然后实现两个 API,一个是 put(key, val) 方法存入键值对,另一个是 get(key) 方法获取 key 对应的 val,如果 key 不存在则返回 -1。
要让 put 和 get 方法的时间复杂度为 O(1)O(1),我们可以总结出 cache 这个数据结构必要的条件:查找快,插入快,删除快,有顺序之分。
那么,什么数据结构同时符合上述条件呢?哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢。所以结合一下,形成一种新的数据结构:哈希链表。
LRU 缓存算法的核心数据结构就是哈希链表,双向链表和哈希表的结合体。
实现代码:
class LRUCache {
private HashMap<Integer, Node> map;
private DoubleList cache;
// 最大容量
private int cap;
public LRUCache(int capacity) {
this.cap = capacity;
map = new HashMap<Integer, Node>();
cache = new DoubleList();
}
public int get(int key) {
// 关键字不存在,返回-1
if (!map.containsKey(key)) {
return -1;
}
// 获取关键字的值
int val = map.get(key).val;
// 把该数据提前
put(key, val);
return val;
}
public void put(int key, int value) {
// 先创建x节点
Node x = new Node(key, value);
// 关键点已经存在
if (map.containsKey(key)) {
// 删除旧节点
cache.remove(map.get(key));
// 把新节点插入头部
cache.addFirst(x);
// 更新map中对应的数据
map.put(key, x);
} else {
// 缓存容量达到上限
if (cap == cache.size()) {
// 删除链表中最后一个节点
Node last = cache.removeLast();
// map删除数据
map.remove(last.key);
}
// 将新节点加入链表头部
cache.addFirst(x);
// 更新map中的值
map.put(key, x);
}
}
}
// 定义双向链表节点(存放键值对)
class Node {
public int key, val;
public Node next, prev;
// 链表节点构造器
public Node(int k, int v) {
this.key = k;
this.val = v;
}
}
// 双向链表
class DoubleList {
// 定义头节点和尾节点
private Node head, tail;
// 链表长度
private int size;
// 无参构造器,初始化创建头节点、尾节点及链表长度
public DoubleList() {
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
size = 0;
}
// 删除链表中的节点
public void remove(Node x) {
x.prev.next = x.next;
x.next.prev = x.prev;
size--;
}
// 在链表头部插入节点
public void addFirst(Node x) {
x.next = head.next;
x.prev = head;
head.next.prev = x;
head.next = x;
size++;
}
// 删除链表中最后一个节点,并返回该节点
public Node removeLast() {
if (tail.prev == head) {
return null;
}
Node last = tail.prev;
remove(last);
return last;
}
// 链表长度
public int size() {
return size;
}
}
复杂度分析
时间复杂度 | O(1) |
---|---|
空间复杂度 | O(1) |
滑动窗口
3 无重复字符的最长子串
题目描述
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
输入: "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 输入: "pwwkew" 输出: 3 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
解法1 滑动窗口
定义一个 map 数据结构存储 (k, v),其中 key 值为字符,value 值为字符位置 +1,加 1 表示从字符位置后一个才开始不重复。我们定义不重复子串的开始位置为 start,结束位置为 end。随着 end 不断遍历向后,会遇到与 [start, end] 区间内字符相同的情况,此时将字符作为 key 值,获取其 value 值,并更新 start,此时 [start, end] 区间内不存在重复字符无论是否更新 start,都会更新其 map 数据结构和结果 ans。题解
实现代码:
class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length(), ans = 0;
Map<Character, Integer> map = new HashMap<>();
for (int start = 0, end = 0; end < n; end++) {
char key = s.charAt(end);
if (map.containsKey(key)) {
start = Math.max(map.get(key), start);
}
ans = Math.max(ans, end - start + 1);
map.put(key, end + 1);
}
return ans;
}
}
复杂度分析
时间复杂度 | O(n) |
---|---|
空间复杂度 | O(min(m,n)) |
字符串
20 有效的括号
题目描述
给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。输入: "()[]{}" 输出: true 输入: "([)]" 输出: false
实现代码
package com.cn.algorithm.string;
import java.util.HashMap;
import java.util.Stack;
/**
* @Author: cyh
* @Date: 2020-04-30 12:40
* @Description: 有效的括号
**/
public class IsValid {
private HashMap<Character, Character> map;
/**
* 不用map
* 复杂度:时间复杂度 o(n) 空间复杂度 o(n)
* @param s
* @return
*/
public boolean isValid(String s) {
if (s.isEmpty()) {
return true;
}
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(') {
stack.push(')');
} else if (c == '{') {
stack.push('}');
} else if (c == '[') {
stack.push(']');
} else if (stack.empty() || c != stack.pop()) {
return false;
}
}
if (stack.empty()) {
return true;
}
return false;
}
/**
* 辅助栈(先通过HashMap存储括号)
* 复杂度:时间复杂度 o(n) 空间复杂度 o(n)
* @param s
* @return
*/
public boolean isValidMap(String s) {
map = new HashMap<>();
map.put(')', '(');
map.put('}', '{');
map.put(']','[');
Stack<Character> stack = new Stack<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (map.containsKey(c)) {
char topElement = stack.empty() ? '#' : stack.pop();
if (topElement != map.get(c)) {
return false;
}
} else {
stack.push(c);
}
}
return stack.isEmpty();
}
public static void main(String[] args) {
String s = "({})";
IsValid demo = new IsValid();
System.out.println(demo.isValid(s));
System.out.println(demo.isValidMap(s));
}
}
双指针
26 删除排序数组中的重复项
题目描述
给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
给定 nums = [0,0,1,1,1,2,2,3,3,4], 函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
实现代码
class Solution {
public int removeDuplicates(int[] nums) {
if (nums.length == 0) return 0;
int p = 0;
for (int q = 1; q < nums.length; q++) {
if (nums[p] != nums[q]) {
p++;
nums[p] = nums[q];
}
}
return p + 1;
}
}
复杂度分析
时间复杂度 | O(n) |
---|---|
空间复杂度 | O(1) |
位操作
136 只出现一次的数字
题目描述
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
输入: [2,2,1] 输出: 1
解法1 异或
异或解法:异或运算满足交换律,a^b^a=a^a^b=b
,因此ans相当于nums[0]^nums[1]^nums[2]^nums[3]^nums[4].....
然后再根据交换律把相等的合并到一块儿进行异或(结果为0),然后再与只出现过一次的元素进行异或,这样最后的结果就是,只出现过一次的元素(0^任意值=任意值)
代码实现
class Solution {
public int singleNumber(int[] nums) {
int ans = nums[0];
if (nums.length > 1) {
for (int i = 1; i < nums.length; i++) {
ans = ans ^ nums[i];
}
}
return ans;
}
}
复杂度分析
时间复杂度 | O(n) |
---|---|
空间复杂度 | O(1) |
解法2 列表
遍历数组,如果数组中的元素不存在在列表中,则把该元素添加到列表中,如果列表中存在该元素,则删除该列表元素。
代码实现
class Solution {
public int singleNumber(int[] nums) {
List<Integer> list = new ArrayList<>();
for (Integer i : nums) {
if (!list.contains(i)) {
list.add(i);
} else {
list.remove((Object)i);
}
}
return list.get(0);
}
}
复杂度分析
时间复杂度 | $O(n^2)$ |
---|---|
空间复杂度 | $O(n)$ |
解法3 哈希表
遍历数组,如果数组中的元素不存在在哈希表中,则把该元素添加到哈希表中,如果哈希表中存在该元素,则删除该哈希表元素。
代码实现
class Solution {
public int singleNumber(int[] nums) {
Map<Integer, Object> map = new HashMap<>();
for (int i : nums) {
if (! map.containsKey(i)) {
map.put(i, null);
} else {
map.remove(i);
}
}
return map.keySet().iterator().next();
}
}
复杂度分析
时间复杂度 | $O(n)$ |
---|---|
空间复杂度 | $O(n)$ |
动态规划
90% 的字符串问题都可以⽤动态规划解决,并且90%是采⽤⼆维数组。
70. 爬楼梯
题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶
解法1 动态规划
这个问题可以被分解为一些包含最优子结构的子问题,即它的最优解可以从其子问题的最优解来有效地构建,我们可以使用动态规划来解决这一问题。
第 i 阶可以由以下两种方法得到:
在第 (i-1)阶后向上爬一阶。
在第 (i-2) 阶后向上爬 2 阶。
所以到达第 i 阶的方法总数就是到第 (i-1) 阶和第 (i-2) 阶的方法数之和。
令 dp[i] 表示能到达第 i 阶的方法总数:dp[i]=dp[i-1]+dp[i-2]
代码实现
class Solution {
public int climbStairs(int n) {
if (n == 1)
return 1;
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[ i - 2];
}
return dp[n];
}
}
复杂度分析
时间复杂度 | $O(n)$ |
---|---|
空间复杂度 | $O(n)$ |
解法2 斐波那契数(优化)
在上述方法中,我们使用 dp数组,其中 dp[i]=dp[i-1]+dp[i-2]。可以很容易通过分析得出 dp[i]其实就是第 i 个斐波那契数。
Fib(n)=Fib(n-1)+Fib(n-2)
现在我们必须找出以 1 和 2 作为第一项和第二项的斐波那契数列中的第 n 个数,也就是说 Fib(1)=1且 Fib(2)=2。
代码实现
class Solution {
public int climbStairs(int n) {
if (n == 1)
return 1;
int first = 1;
int second = 2;
for (int i = 3; i <= n; i++) {
int third = first + second;
first = second;
second = third;
}
return second;
}
}
复杂度分析
时间复杂度 | $O(n)$ |
---|---|
空间复杂度 | $O(1)$ |
62. 不同路径
题目描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?
输入: m = 3, n = 2 输出: 3 解释: 从左上角开始,总共有 3 条路径可以到达右下角。 1. 向右 -> 向右 -> 向下 2. 向右 -> 向下 -> 向右 3. 向下 -> 向右 -> 向右
解法1 动态规划
步骤⼀、定义数组元素的含义
当机器⼈从左上⻆⾛到(i, j) 这个位置时,⼀共有 dp[i] [j] 种路径。这个⽹格相当于⼀个⼆维数组,数组是从下标为 0 开始算起的,所以 右下⻆的位置是 (m-1, n - 1),所以dp[m-1] [n-1]
就是我们要找的答案。
步骤⼆:找出关系数组元素间的关系式
由于机器⼈可以向下⾛或者向右⾛,所以有两种⽅式到达(i,j)这个位置,⼀种是从 (i-1, j) 这个位置⾛⼀步到达,⼀种是从(i, j - 1) 这个位置⾛⼀步到达,因为是计算所有可能的步骤,所以是把所有可能⾛的路径都加起来,所以关系式是dp[i] [j] = dp[i-1] [j] + dp[i] [j-1]
。
步骤三、找出初始值
显然,当 dp[i] [j] 中,如果 i 或者 j 有⼀个为 0,就不能用关系式了,因为这个时候把i - 1 或者 j - 1,就变成负数了,数组就会出问题了,所以我们的初始值是计算出所有的 dp[0] [0….n-1]和所有的 dp[0….m-1] [0]。这个还是⾮常容易计算的,相当于计算机图中的最上⾯⼀⾏和左边⼀列。因此初始值如下:
dp[0] [0….n-1] = 1; // 相当于最上⾯⼀⾏,机器⼈只能⼀直往左⾛
dp[0…m-1] [0] = 1; // 相当于最左⾯⼀列,机器⼈只能⼀直往下⾛
代码实现
class Solution {
// 动态规划 时间复杂度o(m*n) 空间复杂度o(min(m,n))
public int uniquePaths(int m, int n) {
if (m <= 0 || n <= 0) {
return 0;
}
// 初始值
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n; j++) {
dp[0][j] = 1;
}
// 最优子结构
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
复杂度分析
时间复杂度 | $o(m*n)$ |
---|---|
空间复杂度 | $o(min(m,n))$ |
64. 最小路径和
题目描述
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
输入: [ [1,3,1], [1,5,1], [4,2,1] ] 输出: 7 解释: 因为路径 1→3→1→1→1 的总和最小。
解法1 动态规划
步骤⼀、定义数组元素的含义
当从左上⻆⾛到(i, j) 这个位置时,⼀共有 dp[i] [j] 种路径。这个⽹格相当于⼀个⼆维数组,数组是从下标为 0 开始算起的,所以 右下⻆的位置是 (m-1, n - 1),所以dp[m-1] [n-1]
就是我们要找的答案。
步骤⼆:找出关系数组元素间的关系式
由于可以向下⾛或者向右⾛,所以有两种⽅式到达(i,j)这个位置,⼀种是从 (i-1, j) 这个位置⾛⼀步到达,⼀种是从(i, j - 1) 这个位置⾛⼀步到达,因为是计算所有可能的最小路径和,所以要选择一种到达方式值最小的,所以关系式是dp[i] [j] = min(dp[i-1][j],dp[i][j-1]) + grid[i][j]
。
步骤三、找出初始值
显然,当 dp[i] [j] 中,如果 i 或者 j 有⼀个为 0,就不能用关系式了,因为这个时候把i - 1 或者 j - 1,就变成负数了,数组就会出问题了,所以我们的初始值是计算出所有的 dp[0] [0….n-1]和所有的 dp[0….m-1] [0]。因此初始值如下:
dp[0] [j] = grid[0] [j] + dp[0] [j-1]; // 相当于最上⾯⼀⾏
dp[i] [0] = grid[i] [0] + dp[i] [0]; // 相当于最左⾯⼀列
代码实现
class Solution {
// 动态规划
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
if (m <= 0 || n <= 0) {
return 0;
}
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
// 临界值
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i-1][0] + grid[i][0];
}
for (int j = 1; j < n; j++) {
dp[0][j] = dp[0][j-1] + grid[0][j];
}
// 关系式
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i][j-1],dp[i-1][j]) + grid[i][j];
}
}
return dp[m-1][n-1];
}
}
复杂度分析
时间复杂度 | $o(m*n)$ |
---|---|
空间复杂度 | $o(min(m,n))$ |
72. 编辑距离
题目描述
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:插入一个字符 删除一个字符 替换一个字符
输入:word1 = "horse", word2 = "ros" 输出:3 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')
解法1 动态规划
步骤⼀、定义数组元素的含义
由于我们的⽬的求将 word1 转换成 word2 所使⽤的最少操作数 。那我们就定义 dp[i] [j]的含义为:当字符串 word1 的⻓度为 i,字符串 word2 的⻓度为 j 时,将 word1 转化为 word2 所使⽤的最少操作次数为 dp[i] [j]
步骤⼆:找出关系数组元素间的关系式
由于我们是要让操作的次数最⼩,所以我们要寻找最佳操作。那么有如下关系式:
⼀、如果我们 word1[i] 与 word2 [j] 相等,这个时候不需要进⾏任何操作,显然有 dp[i] [j] = dp[i-1] [j-1]。
⼆、如果我们 word1[i] 与 word2 [j] 不相等,这个时候我们就必须进⾏调整,⽽调整的操作有 3 种,我
们要选择⼀种。三种操作对应的关系试如下(注意字符串与字符的区别):
(1)、如果把字符 word1[i] 替换成与 word2[j] 相等,则有 dp[i] [j] = dp[i-1] [j-1] + 1;
(2)、如果在字符串 word1末尾插⼊⼀个与 word2[j] 相等的字符,则有 dp[i] [j] = dp[i] [j-1] + 1;
(3)、如果把字符 word1[i] 删除,则有 dp[i] [j] = dp[i-1] [j] + 1;
那么我们应该选择⼀种操作,使得 dp[i] [j] 的值最⼩,显然有
dp[i] [j] = min(dp[i-1] [j-1],dp[i] [j-1],dp[[i-1] [j]]) + 1;
步骤三、找出初始值
显然,当 dp[i] [j] 中,如果 i 或者 j 有⼀个为 0,就不能用关系式了,因为这个时候把i - 1 或者 j - 1,就变成负数了,数组就会出问题了,所以我们的初始值是计算出所有的 dp[0] [0….n-1]和所有的 dp[0….m-1] [0]。当有⼀个字符串的⻓度为 0 时,转化为另外⼀个字符串,那就只能⼀直进⾏插⼊或者删除操作了。
代码实现
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m+1][n+1];
for (int i = 1; i <= m; i++) {
dp[i][0] = dp[i-1][0] + 1;
}
for (int j = 1; j <= n; j++) {
dp[0][j] = dp[0][j-1] + 1;
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i-1) == word2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1];
} else {
dp[i][j] = Math.min(Math.min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1]) + 1;
}
}
}
return dp[m][n];
}
}
复杂度分析
时间复杂度 | $o(m*n)$ |
---|---|
空间复杂度 | $o(min(m,n))$ |
198. 打家劫舍
题目描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
输入: [1,2,3,1] 输出: 4 解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4
解法1 动态规划
1、如果只有两个房间,谁大选谁
2、如果有三个房间,要考虑两种情况:偷第三个房间的话,则偷盗的金额为第一个房间的金额加第三个房间的金额;如果不偷第三个房间的话,则偷盗的金额为前两个房间中金额最大的
初始状态:
dp[0]=nums[0]
dp[1]= Max(nums[0],nums[1])
状态转移方程:
i>2 dp[i] = Max((nums[i]+dp[i-2]),dp[i-1])
代码实现
class Solution {
// 动态规划
public int rob(int[] nums) {
if (nums.length == 0) return 0;
int[] dp = new int[nums.length];
dp[0] = nums[0];
if (nums.length < 2) return nums[0];
dp[1] = nums[0] > nums[1] ? nums[0] : nums[1];
for (int i = 2; i < nums.length; i++) {
dp[i] = (nums[i] + dp[i - 2]) > dp[i - 1] ? nums[i] + dp[i - 2] : dp[i - 1];
}
return dp[nums.length - 1];
}
}
复杂度分析
时间复杂度 | $o(n)$ |
---|---|
空间复杂度 | $o(1)$ |
53. 最大子序和✨
题目描述
给定一个整数数组
nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。输入: [-2,1,-3,4,-1,2,1,-5,4], 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
解法1 动态规划
1、如果只考虑第一个元素,则最大子序和为本身dp[0]=nums[0]
2、如果考虑前两个元素,则最大子序和为dp[1]=max(nums[0],nums[1],nums[0]+nums[1])
3、如果考虑前三个元素,则最大子序和为max(dp[1]+nums[2],nums[2])
初始状态:
dp[0]=nums[0]
dp[1]=Max(dp[0]+nums[1],nums[1])
状态转移方程:
dp[i] = Max((nums[i]+dp[i-1]),nums[i])
代码实现
class Solution {
public int maxSubArray(int[] nums) {
if (nums.length == 0) return 0;
int[] dp = new int[nums.length];
dp[0] = nums[0];
int max = dp[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = (dp[i-1] + nums[i]) > nums[i] ? dp[i-1] + nums[i] : nums[i];
if (dp[i] > max) {
max = dp[i];
}
}
return max;
}
}
复杂度分析
时间复杂度 | $o(n)$ |
---|---|
空间复杂度 | $o(1)$ |
递归
101. 对称二叉树
题目描述
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树
[1,2,2,3,4,4,3]
是对称的。1 / \ 2 2 / \ / \ 3 4 4 3
解法1 递归
递归结束条件:都为空指针则返回 true;只有一个为空则返回 false
递归过程:判断两个指针当前节点值是否相等;判断 A 的右子树与 B 的左子树是否对称;判断 A 的左子树与 B 的右子树是否对称
代码实现
class Solution {
public boolean isSymmetric(TreeNode root) {
return isMirror(root, root);
}
public boolean isMirror(TreeNode t1, TreeNode t2) {
if (t1 == null && t2 == null) return true;
if (t1 == null || t2 == null) return false;
return (t1.val == t2.val) && isMirror(t1.left, t2.right) && isMirror(t1.right, t2.left);
}
}
复杂度分析
时间复杂度 | $O(n)$ |
---|---|
空间复杂度 | $O(n)$ |
贪心算法
121. 买卖股票的最佳时机
题目描述
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票
输入: [7,1,5,3,6,4] 输出: 5 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
解法1 贪心算法
从左向右,维护一个最小值low,与每一天的股票价格做差,差最大的为答案。
代码实现
class Solution {
// 贪心法 时间复杂度o(n) 空间复杂度o(1)
public int maxProfit(int[] prices) {
int maxProfit = 0;
int low = Integer.MAX_VALUE;
for (int p : prices) {
if (p < low)
low = p;
maxProfit = Math.max(maxProfit, p - low);
}
return maxProfit;
}
}
复杂度分析
时间复杂度 | $O(n)$ |
---|---|
空间复杂度 | $O(1)$ |
排序
169. 多数元素
题目描述
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
输入: [2,2,1,1,1,2,2] 输出: 2
解法1 排序✨
代码实现
class Solution {
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length/2];
}
}
复杂度分析
时间复杂度 | $O(nlogn)$ |
---|---|
空间复杂度 | $O(logn)$ |
解法2 哈希表
在哈希映射的键值对中,键表示元素,值表示出现的次数,遍历数组中的元素,如果map中存在,就让这个元素对应的值加1。最后再返回map中的最大值。
代码实现
class Solution {
public int majorityElement(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(nums[i])) {
map.put(nums[i], map.get(nums[i]) + 1);
} else {
map.put(nums[i], 1);
}
}
Map.Entry<Integer, Integer> maxMap = null;
for (Map.Entry<Integer, Integer> value : map.entrySet()) {
if (maxMap == null || value.getValue() > maxMap.getValue()) {
maxMap = value;
}
}
return maxMap.getKey();
}
}
复杂度分析
时间复杂度 | $O(n)$ |
---|---|
空间复杂度 | $O(n)$ |
解法3 计数法
扫描一次数组,扫描的过程中记录 “当前数”curNum 和 “当前数的个数”count 。如果遇到不相同的数,则count减1,count减到0时,curNum换成扫描到的新数。扫描完一遍数组,最后的curNum就是结果。
代码实现
class Solution {
public int majorityElement(int[] nums) {
int curNum = nums[0];
int count = 1;
for (int i = 1; i < nums.length; i++) {
if (count == 0) {
curNum = nums[i];
count = 1;
} else {
if (curNum != nums[i]) count--;
else count++;
}
}
return curNum;
}
}
复杂度分析
时间复杂度 | $O(n)$ |
---|---|
空间复杂度 | $O(1)$ |
4. 寻找两个有序数组的中位数
题目描述
给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。你可以假设 nums1 和 nums2 不会同时为空。
nums1 = [1, 2] nums2 = [3, 4] 则中位数是 (2 + 3)/2 = 2.5
解法1 合并数组
先将两个数组排序合并,然后根据奇数,还是偶数,返回中位数。
代码实现
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
int[] nums = new int[m+n];
if (m == 0) {
if (n % 2 == 0) {
return (nums2[n / 2 - 1] + nums2[n / 2]) / 2.0;
} else {
return nums2[n / 2];
}
}
if (n == 0) {
if (m % 2 == 0) {
return (nums1[m / 2 - 1] + nums1[m / 2]) / 2.0;
} else {
return nums1[m / 2];
}
}
int count = 0;
int i = 0, j = 0;
while (count != (m + n)) {
if (i == m) {
while (j != n) {
nums[count++] = nums2[j++];
}
break;
}
if (j == n) {
while (i != m) {
nums[count++] = nums1[i++];
}
break;
}
if (nums1[i] > nums2[j]) {
nums[count++] = nums2[j++];
} else {
nums[count++] = nums1[i++];
}
}
if (count % 2 == 0) {
return (nums[count / 2 - 1] + nums[count / 2]) / 2.0;
}
else
return nums[count / 2];
}
}
复杂度分析
时间复杂度 | $O(m+n)$ |
---|---|
空间复杂度 | $O(m+n)$ |
解法2 求第k小数(没明白,没写)
在哈希映射的键值对中,键表示元素,值表示出现的次数,遍历数组中的元素,如果map中存在,就让这个元素对应的值加1。最后再返回map中的最大值。
代码实现
class Solution {
public int majorityElement(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(nums[i])) {
map.put(nums[i], map.get(nums[i]) + 1);
} else {
map.put(nums[i], 1);
}
}
Map.Entry<Integer, Integer> maxMap = null;
for (Map.Entry<Integer, Integer> value : map.entrySet()) {
if (maxMap == null || value.getValue() > maxMap.getValue()) {
maxMap = value;
}
}
return maxMap.getKey();
}
}
复杂度分析
时间复杂度 | $O(n)$ |
---|---|
空间复杂度 | $O(n)$ |
解法3 计数法
扫描一次数组,扫描的过程中记录 “当前数”curNum 和 “当前数的个数”count 。如果遇到不相同的数,则count减1,count减到0时,curNum换成扫描到的新数。扫描完一遍数组,最后的curNum就是结果。
代码实现
class Solution {
public int majorityElement(int[] nums) {
int curNum = nums[0];
int count = 1;
for (int i = 1; i < nums.length; i++) {
if (count == 0) {
curNum = nums[i];
count = 1;
} else {
if (curNum != nums[i]) count--;
else count++;
}
}
return curNum;
}
}
复杂度分析
时间复杂度 | $O(n)$ |
---|---|
空间复杂度 | $O(1)$ |