LeetCode(Java版)
两数之和
题目描述
给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。
你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
解法
利用 HashMap 记录数组元素值和对应的下标,对于一个数 nums[i],判断 target - nums[i] 是否存在 HashMap 中,存在的话,返回两个下标组成的数组。注意,已存在的元素下标在前,当前元素下标在后。
```java
class Solution {
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; ++i) {
if (map.containsKey(target - nums[i])) {
return new int[] {map.get(target - nums[i]), i};
}
map.put(nums[i], i);
}
return null;
}
}
两数相加
题目描述
给定两个非空链表来表示两个非负整数。位数按照逆序方式存储,它们的每个节点只存储单个数字。将两数相加返回一个新的链表。
你可以假设除了数字 0 之外,这两个数字都不会以零开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
解法
同时遍历两个链表,对应值相加(还有 quotient)求余数得到值并赋给新创建的结点。而商则用quotient存储,供下次相加。
Java
初始版本:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode res = new ListNode(-1);
ListNode cur = res;
int quotient = 0;
int t = 0;
while (l1 != null && l2 != null) {
t = l1.val + l2.val + quotient;
quotient = t / 10;
ListNode node = new ListNode(t % 10);
cur.next = node;
l1 = l1.next;
l2 = l2.next;
cur = node;
}
while (l1 != null) {
t = l1.val + quotient;
quotient = t / 10;
ListNode node = new ListNode(t % 10);
cur.next = node;
l1 = l1.next;
cur = node;
}
while (l2 != null) {
t = l2.val + quotient;
quotient = t / 10;
ListNode node = new ListNode(t % 10);
cur.next = node;
l2 = l2.next;
cur = node;
}
if (quotient != 0) {
cur.next = new ListNode(quotient);
cur = cur.next;
}
return res.next;
}
}
简化版本:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode res = new ListNode(-1);
ListNode cur = res;
int quotient = 0;
while (l1 != null || l2 != null || quotient != 0) {
int t = (l1 == null ? 0 : l1.val) + (l2 == null ? 0 : l2.val) + quotient;
quotient = t / 10;
ListNode node = new ListNode(t % 10);
cur.next = node;
cur = node;
l1 = (l1 == null) ? l1 : l1.next;
l2 = (l2 == null) ? l2 : l2.next;
}
return res.next;
}
}
CPP
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode *ans_l = new ListNode(0);
ListNode *head = ans_l;
int tmp = 0;
while(l1 != NULL && l2 != NULL){
tmp += l1->val + l2->val;
ans_l->next = new ListNode(tmp % 10);
tmp = tmp / 10;
ans_l = ans_l->next;
l1 = l1->next;
l2 = l2->next;
}
while(l1 != NULL){
tmp += l1->val;
ans_l->next = new ListNode(tmp % 10);
tmp = tmp / 10;
ans_l = ans_l->next;
l1 = l1->next;
}
while(l2 != NULL){
tmp += l2->val;
ans_l->next = new ListNode(tmp % 10);
tmp = tmp / 10;
ans_l = ans_l->next;
l2 = l2->next;
}
if(tmp)ans_l->next = new ListNode(tmp);
return head->next;
}
};
无重复字符的最长子串
题目描述
给定一个字符串,找出不含有重复字符的最长子串的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 无重复字符的最长子串是 "abc",其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 无重复字符的最长子串是 "b",其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 无重复字符的最长子串是 "wke",其长度为 3。
请注意,答案必须是一个子串,"pwke" 是一个子序列 而不是子串。
解法
利用指针 p
, q
,初始指向字符串开头。遍历字符串,q
向右移动,若指向的字符在 map 中,说明出现了重复字符,此时,p
要在出现重复字符的下一个位置 map.get(chars[q]) + 1
和当前位置 p
之间取较大值,防止 p
指针回溯。循环的过程中,要将 chars[q] 及对应位置放入 map 中,也需要不断计算出max
与 q - p + 1
的较大值,赋给 max
。最后输出 max
即可。
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0) {
return 0;
}
char[] chars = s.toCharArray();
int len = chars.length;
int p = 0, q = 0;
int max = 0;
Map<Character, Integer> map = new HashMap<>();
while (q < len) {
if (map.containsKey(chars[q])) {
// 防止p指针回溯,导致计算到重复字符的长度
// eg. abba,当q指向最右的a时,若简单把p赋为map.get(chars[q] + 1),则出现指针回溯
p = Math.max(p, map.get(chars[q]) + 1);
}
map.put(chars[q], q);
max = Math.max(max, q - p + 1);
++q;
}
return max;
}
}
两个排序数组的中位数
题目描述
给定两个大小为 m 和 n 的有序数组 nums1 和 nums2 。
请找出这两个有序数组的中位数。要求算法的时间复杂度为 O(log (m+n)) 。
你可以假设 nums1 和 nums2 不同时为空。
示例 1:
nums1 = [1, 3]
nums2 = [2]
中位数是 2.0
示例 2:
nums1 = [1, 2]
nums2 = [3, 4]
中位数是 (2 + 3)/2 = 2.5
解法
假设两数组长度分别为 len1, len2,分别将 num1, num2 切成左右两半。
举个栗子:
nums1: num1[0] num1[1] num1[2]......num1[i - 1] | num1[i] ...nums1[len1 - 2] nums1[len1 - 1]
nums2: nums2[0] nums2[1] nums2[2]......nums2[j - 1] | nums2[j] ...nums2[len2 - 2] nums2[len2 - 1]
num1 在[0, i - 1] 是左半部分,[i, len1 - 1] 是右半部分;
num2 在[0, j - 1] 是左半部分,[j, len2 - 1] 是右半部分。
若两个左半部分合起来的最大值 <=
右半部分合起来的最小值。那么中位数就可以直接拿到了。
- 若 nums1[i - 1] > nums2[j],说明 num1 的左边有数据过大,应该放到右边,而这样会使左边总数少了,那么 num2 右边的一个给左边就平衡了。如下:
nums1: num1[0] num1[1] num1[2]......num1 | [i - 1] num1[i] ...nums1[len1 - 2] nums1[len1 - 1]
nums2: nums2[0] nums2[1] nums2[2]......nums2[j - 1] nums2[j] | ...nums2[len2 - 2] nums2[len2 - 1]
-
若 nums2[j - 1] > nums1[i],同理。
-
否则,计算中位数。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
if (len1 > len2) {
int[] tmp = nums1;
nums1 = nums2;
nums2 = tmp;
int t = len1;
len1 = len2;
len2 = t;
}
int min = 0;
int max = len1;
int m = (len1 + len2 + 1) / 2;
while (min <= max) {
int i = (min + max) / 2;
int j = m - i;
if (i > min && nums1[i - 1] > nums2[j]) {
--max;
} else if (i < max && nums2[j - 1] > nums1[i]) {
++min;
} else {
int maxLeft = i == 0 ? nums2[j - 1] : j == 0 ? nums1[i - 1] : Math.max(nums1[i - 1], nums2[j - 1]);
if (((len1 + len2) & 1) == 1) {
return maxLeft;
}
int minRight = i == len1 ? nums2[j] : j == len2 ? nums1[i] : Math.min(nums2[j], nums1[i]);
return (maxLeft + minRight) / 2.0;
}
}
return 0;
}
}
最长回文子串
题目描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba"也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
解法
- 解法1
利用动态规划,二维数组 res
存储 [j, i]
区间是否为回文串。动态规划递推式:
res[j][i] = res[j + 1][i - 1] && chars[j] == chars[i]
此方法时间和空间复杂度均为 O(n²)
。
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) {
return "";
}
String str = "";
char[] chars = s.toCharArray();
int len = chars.length;
boolean[][] res = new boolean[len][len];
int start = 0;
int max = 1;
for (int i = 0; i < len; ++i) {
for (int j = 0; j <= i; ++j) {
res[j][i] = i - j < 2
? chars[j] == chars[i]
: res[j + 1][i - 1] && chars[j] == chars[i];
if (res[j][i] && max < i - j + 1) {
max = i - j + 1;
start = j;
}
}
}
return s.substring(start, start + max);
}
}
Z 字形变换
题目描述
将一个给定字符串根据给定的行数,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 "LEETCODEISHIRING"
行数为 3 时,排列如下:
L C I R
E T O E S I I G
E D H N
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"LCIRETOESIIGEDHN"
。
请你实现这个将字符串进行指定行数变换的函数:
string convert(string s, int numRows);
示例 1:
输入: s = "LEETCODEISHIRING", numRows = 3
输出: "LCIRETOESIIGEDHN"
示例 2:
输入: s = "LEETCODEISHIRING", numRows = 4
输出: "LDREOEIIECIHNTSG"
解释:
L D R
E O E I I
E C I H N
T S G
解法
class Solution {
public String convert(String s, int numRows) {
if (numRows == 1) return s;
StringBuilder result = new StringBuilder();
int group = 2 * numRows - 2;
for (int i = 1; i <= numRows; i++) {
int interval = 2 * numRows - 2 * i;
if (i == numRows) interval = 2 * numRows - 2;
int index = i;
while (index <= s.length()) {
result.append(s.charAt(index - 1));
index += interval;
interval = group - interval;
if (interval == 0) interval = group;
}
}
return result.toString();
}
}
反转整数
题目描述
给定一个 32 位有符号整数,将整数中的数字进行反转。
示例 1:
输入: 123
输出: 321
示例 2:
输入: -123
输出: -321
示例 3:
输入: 120
输出: 21
注意:
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−231, 231 − 1]。根据这个假设,如果反转后的整数溢出,则返回 0。
解法
- 解法1
用 long 型存储该整数,取绝对值,然后转成 StringBuilder 进行 reverse,后转回 int。注意判断该数是否在[Integer.MIN_VALUE, Intger.MAX_VALUE] 范围内。
class Solution {
public int reverse(int x) {
if (x == 0) {
return x;
}
long tmp = x;
boolean isPositive = true;
if (tmp < 0) {
isPositive = false;
tmp = -tmp;
}
long val = Long.parseLong(new StringBuilder(String.valueOf(tmp)).reverse().toString());
return isPositive ? (val > Integer.MAX_VALUE ? 0 : (int) val) : (-val < Integer.MIN_VALUE ? 0 : (int) (-val));
}
}
- 解法2
循环对数字求 %, /
,累加,最后返回结果。注意判断值是否溢出。
class Solution {
public int reverse(int x) {
long res = 0;
// 考虑负数情况,所以这里条件为: x != 0
while (x != 0) {
res = res * 10 + (x % 10);
x /= 10;
}
return (res < Integer.MIN_VALUE || res > Integer.MAX_VALUE)
? 0
: (int) res;
}
}
回文数
题目描述
判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
示例 1:
输入: 121
输出: true
示例 2:
输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例 3:
输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。
进阶:
你能不将整数转为字符串来解决这个问题吗?
解法
负数直接返回 false。对于非负数,每次取最后一位y % 10
,累加到 res * 10
,之后 y /= 10
,直到 y == 0
。判断此时 res 与 x 是否相等。
class Solution {
public boolean isPalindrome(int x) {
if (x < 0) {
return false;
}
int res = 0;
int y = x;
while (y != 0) {
res = res * 10 + y % 10;
y /= 10;
}
return res == x;
}
}
罗马数字转整数
题目描述
罗马数字包含以下七种字符:I
, V
, X
, L
,C
,D
和 M
。
字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
例如, 罗马数字 2 写做 II
,即为两个并列的 1。12 写做 XII
,即为 X
+ II
。 27 写做 XXVII
, 即为 XX
+ V
+ II
。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII
,而是 IV
。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX
。这个特殊的规则只适用于以下六种情况:
-
I
可以放在V
(5) 和X
(10) 的左边,来表示 4 和 9。 -
X
可以放在L
(50) 和C
(100) 的左边,来表示 40 和 90。 -
C
可以放在D
(500) 和M
(1000) 的左边,来表示 400 和 900。
给定一个罗马数字,将其转换成整数。输入确保在 1 到 3999 的范围内。
示例 1:
输入: "III"
输出: 3
示例 2:
输入: "IV"
输出: 4
示例 3:
输入: "IX"
输出: 9
示例 4:
输入: "LVIII"
输出: 58
解释: C = 100, L = 50, XXX = 30, III = 3.
示例 5:
输入: "MCMXCIV"
输出: 1994
解释: M = 1000, CM = 900, XC = 90, IV = 4.
解法
用 map 存储字符串及对应的值,遍历 s
,若 s[i, i + 1] 在 map 中,累加对应的值,i 向右移动两格;否则累加 s[i],i 向右移动一格。
class Solution {
public int romanToInt(String s) {
Map<String, Integer> map = new HashMap<String, Integer>(13) {{
put("I", 1);
put("V", 5);
put("X", 10);
put("L", 50);
put("C", 100);
put("D", 500);
put("M", 1000);
put("IV", 4);
put("IX", 9);
put("XL", 40);
put("XC", 90);
put("CD", 400);
put("CM", 900);
}};
int res = 0;
int len = s.length();
for (int i = 0; i < len; ++i) {
if (i != len - 1 && map.get(s.substring(i, i + 2)) != null) {
res += map.get(s.substring(i, i + 2));
++i;
continue;
}
res += map.get(s.substring(i, i + 1));
}
return res;
}
}
最长公共前缀
题目描述
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。
示例 1:
输入: ["flower","flow","flight"]
输出: "fl"
示例 2:
输入: ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。
说明:
所有输入只包含小写字母 a-z 。
解法
取字符串数组第一个元素,遍历每一个字符,与其他每个字符串的对应位置字符做比较,如果不相等,退出循环,返回当前子串。
注意:其他字符串的长度可能小于第一个字符串长度,所以要注意数组越界异常。
class Solution {
public String longestCommonPrefix(String[] strs) {
if (strs == null || strs.length == 0) {
return "";
}
if (strs.length == 1) {
return strs[0];
}
char[] chars = strs[0].toCharArray();
int i = 0;
boolean flag = true;
for (; i < chars.length; ++i) {
char ch = chars[i];
for (int j = 1; j < strs.length; ++j) {
if (strs[j].length() <= i) {
flag = false;
break;
}
if (strs[j].charAt(i) != ch) {
flag = false;
break;
}
}
if (!flag) {
break;
}
}
return strs[0].substring(0, i);
}
}
三数之和
题目描述
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
例如, 给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
解法
先对数组进行排序,遍历数组,固定第一个数i。利用两个指针 p, q 分别指示 i+1, n-1。如果三数之和为0,移动 p, q;如果大于 0,左移 q;如果小于 0,右移 p。遍历到 nums[i] > 0 时,退出循环。
还要注意过滤重复元素。
Java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
int n = nums.length;
if (n < 3) {
return list;
}
int p = 0;
int q = 0;
for (int i = 0; i < n - 2; ++i) {
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
p = i + 1;
q = n - 1;
while (p < q) {
int val = nums[p] + nums[q] + nums[i];
if (val == 0) {
list.add(Arrays.asList(nums[i], nums[p], nums[q]));
++p;
while (p < q && nums[p] == nums[p - 1]) {
++p;
}
--q;
while (p < q && nums[q] == nums[q + 1]) {
--q;
}
} else {
q = val > 0 ? q - 1 : q;
p = val < 0 ? p + 1 : p;
}
}
}
return list;
}
}
CPP
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(),nums.end());
vector<vector<int>> ans;
int sum;
int len = nums.size();
int left,right;
for(int i = 0; i< len;i++){
left = i + 1;
right = len - 1;
while(left < right){
sum = nums[i] + nums[left] + nums[right];
if(sum == 0){
vector<int> vec;
vec.push_back(nums[i]);
vec.push_back(nums[left]);
vec.push_back(nums[right]);
ans.push_back(vec);
while(left < right && nums[left] == nums[left + 1])left++;
while(left < right && nums[right] == nums[right - 1])right--;
left++;
right--;
}
if(sum > 0)right--;
if(sum < 0)left++;
}
while(i<len-1 && nums[i] == nums[i+1])i++;
}
return ans;
}
};
四数之和
题目描述
给定一个包含 n 个整数的数组 nums
和一个目标值 target
,判断 nums
中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target
相等?找出所有满足条件且不重复的四元组。
注意:
答案中不可以包含重复的四元组。
示例:
给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。
满足要求的四元组集合为:
[
[-1, 0, 0, 1],
[-2, -1, 1, 2],
[-2, 0, 0, 2]
]
解法
解法一
-
将数组排序;
-
先假设确定一个数 nums[i] 将 4Sum 问题转换为 3Sum 问题;
-
再假设确定一个数将 3Sum 问题转换为 2Sum 问题;
-
对排序数组,用首尾指针向中间靠拢的思路寻找满足 target 的 nums[l] 和 nums[k]
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> re = new ArrayList<>();
if (nums == null || nums.length < 4) {
return re;
}
Arrays.sort(nums);
for (int i = 0; i < nums.length - 3; i++) {
// 当 nums[i] 对应的最小组合都大于 target 时,后面大于 nums[i] 的组合必然也大于 target,
if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
break;
}
// 当 nums[i] 对应的最大组合都小于 target 时, nums[i] 的其他组合必然也小于 target
if (nums[i] + nums[nums.length - 3] + nums[nums.length - 2] + nums[nums.length - 1] < target) {
continue;
}
int firstNum = nums[i];
for (int j = i + 1; j < nums.length - 2; j++) {
// nums[j] 过大时,与 nums[i] 过大同理
if (nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
break;
}
// nums[j] 过小时,与 nums[i] 过小同理
if (nums[i] + nums[j] + nums[nums.length - 2] + nums[nums.length - 1] < target) {
continue;
}
int twoSum = target - nums[i] - nums[j];
int l = j + 1;
int k = nums.length - 1;
while (l < k) {
int tempSum = nums[l] + nums[k];
if (tempSum == twoSum) {
ArrayList<Integer> oneGroup = new ArrayList<>(4);
oneGroup.add(nums[i]);
oneGroup.add(nums[j]);
oneGroup.add(nums[l++]);
oneGroup.add(nums[k--]);
re.add(oneGroup);
while (l < nums.length && l < k && nums[l] == oneGroup.get(2) && nums[k] == oneGroup.get(3)) {
l++;
k--;
}
} else if (tempSum < twoSum) {
l++;
} else {
k--;
}
}
// 跳过重复项
while ((j < nums.length - 2) && (twoSum + nums[i] + nums[j + 1] == target)) {
j++;
}
}
// 跳过重复项
while (i < nums.length - 3 && nums[i + 1] == firstNum) {
i++;
}
}
return re;
}
}
解法二
对数组进行排序,利用指针 i
, j
固定前两个数,p
, q
指向剩余数组的首尾,判断四数和是否为 target
:
-
若是,添加到
list
中。此时 右移p
直到nums[p] != nums[p - 1]
(为了去重)。同样,q
左移,进行去重。 -
若四数和大于
target
,q
指针左移;否则p
指针右移。 -
对于外面的两层
for
循环,同样需要进行去重操作。
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
int n = nums.length;
List<List<Integer>> list = new ArrayList<>();
int p = 0;
int q = 0;
for (int i = 0; i < n - 3; ++i) {
for (int j = i + 1; j < n - 2; ++j) {
p = j + 1;
q = n - 1;
while (p < q) {
int val = nums[i] + nums[j] + nums[p] + nums[q];
if (val == target) {
list.add(Arrays.asList(nums[i], nums[j], nums[p], nums[q]));
// p 指针右移,直到 nums[p] 与 nums[p - 1] 不等
++p;
while (p < q && nums[p] == nums[p - 1]) {
++p;
}
--q;
while (p < q && nums[q] == nums[q + 1]) {
--q;
}
} else if (val > target) {
--q;
} else {
q = val > target ? q - 1 : q;
p = val < target ? p + 1 : p;
}
}
// j < n - 3:保证 j 不会溢出
while (j < n - 3 && nums[j] == nums[j + 1]) {
++j;
}
}
// i < n - 4:保证 i 不会溢出
while (i < n - 4 && nums[i] == nums[i + 1]) {
++i;
}
}
return list;
}
}
删除链表的倒数第N个节点
题目描述
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。
示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
说明:
给定的 n 保证是有效的。
进阶:
你能尝试使用一趟扫描实现吗?
解法
快指针 fast 先走 n 步,接着快指针 fast 与慢指针 slow 同时前进,等到快指针指向链表最后一个结点时,停止前进。然后将 slow 的 next 指向 slow.next.next,即删除了第 n 个结点。最后返回头指针。
这里设置了 pre 虚拟结点(指向 head )是为了方便处理只有一个结点的情况。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre = new ListNode(-1);
pre.next = head;
ListNode fast = pre;
ListNode slow = pre;
// 快指针先走 n 步
for (int i = 0; i < n; ++i) {
fast = fast.next;
}
while (fast.next != null) {
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return pre.next;
}
}
有效的括号
题目描述
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
示例 1:
输入: "()"
输出: true
示例 2:
输入: "()[]{}"
输出: true
示例 3:
输入: "(]"
输出: false
示例 4:
输入: "([)]"
输出: false
示例 5:
输入: "{[]}"
输出: true
解法
遍历 string,遇到左括号,压入栈中;遇到右括号,从栈中弹出元素,元素不存在或者元素与该右括号不匹配,返回 false。遍历结束,栈为空则返回 true,否则返回 false。
因为字符串只包含"(){}[]",也可以进行特殊处理,用映射来做。
Java 版实现
class Solution {
public boolean isValid(String s) {
if (s == null || s == "") {
return true;
}
char[] chars = s.toCharArray();
int n = chars.length;
Stack<Character> stack = new Stack<>();
for (int i = 0; i < n; ++i) {
char a = chars[i];
if (isLeft(a)) {
stack.push(a);
} else {
if (stack.isEmpty() || !isMatch(stack.pop(), a)) {
return false;
}
}
}
return stack.isEmpty();
}
private boolean isMatch(char a, char b) {
return (a == '(' && b == ')') || (a == '[' && b == ']') || (a == '{' && b == '}');
}
private boolean isLeft(char a) {
return a == '(' || a == '[' || a == '{';
}
private boolean isRight(char a) {
return a == ')' || a == ']' || a == '}';
}
}
C++ 版实现
class Solution {
public:
bool isValid(string s) {
stack<char> _stack;
int len = s.length();
if(len == 0)return true;
char ch;
for(int i= 0;i<len;i++)
{
if(s[i] == '{' ||s[i] == '['||s[i] == '(' )
{
_stack.push(s[i]);
}
if(s[i] == '}')
{
if(_stack.empty())return false;
else ch = _stack.top();
if(ch != '{')return false;
else _stack.pop();
}
else if(s[i] == ']')
{
if(_stack.empty())return false;
else ch = _stack.top();
if(ch != '[')return false;
else _stack.pop();
}
else if(s[i] == ')')
{
if(_stack.empty())return false;
else ch = _stack.top();
if(ch != '(')return false;
else _stack.pop();
}
}
if(!_stack.empty())return false;
return true;
}
};
// 特殊
class Solution {
public:
bool isValid(string s) {
map<char,int> m={
{'[',1},
{']',-1},
{'{',2},
{'}',-2},
{'(',3},
{')',-3}
};
stack<int> sk;
for(int i=0;i<s.length();i++){
if(m[s[i]]<0 ){
if(!sk.empty() && sk.top()==(-m[s[i]])){
sk.pop();
}else{
return false;
}
}else{
sk.push(m[s[i]]);
}
}
if(sk.empty())
return true;
return false;
}
};
合并两个有序链表
题目描述
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
解法
利用链表天然的递归性。如果 l1 为空,返回 l2;如果 l2 为空,返回 l1。如果 l1.val < l2.val
,返回 l1->mergeTwoLists(l1.next, l2);否则返回 l2->mergeTwoLists(l1, l2.next)。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
合并K个排序链表
题目描述
合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
示例:
输入:
[
1->4->5,
1->3->4,
2->6
]
输出: 1->1->2->3->4->4->5->6
解法
从链表数组索引 0 开始,合并前后相邻两个有序链表,放在后一个链表位置上,依次循环下去...最后 lists[len - 1] 即为合并后的链表。注意处理链表数组元素小于 2 的情况。
思路1: 170ms
用第一个链依次和后面的所有链进行双链合并,利用021的双顺序链合并,秒杀!但是效率极低
时间复杂度是O(x(a+b) + (x-1)(a+b+c) + ... + 1 * (a+b+...+z);[a-z]是各链表长度,x表示链表个数-1
可见时间复杂度是极大的
思路2: 20ms
1.因为链表有序,所以用每个链表的首元素构建初试堆(小顶堆) -- 的队列
2.首元素出队,该元素next指向元素入队
时间复杂度是O(n)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) {
return null;
}
int len = lists.length;
if (len == 1) {
return lists[0];
}
// 合并前后两个链表,结果放在后一个链表位置上,依次循环下去
for (int i = 0; i < len - 1; ++i) {
lists[i + 1] = mergeTwoLists(lists[i], lists[i + 1]);
}
return lists[len - 1];
}
/**
* 合并两个有序链表
* @param l1
* @param l2
* @return listNode
*/
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
}
if (l2 == null) {
return l1;
}
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
CPP
class compare
{
public:
bool operator()(ListNode *l1,ListNode *l2){
//if(!l1 || !l2)
// return !l1;
if(l1 == NULL)return 1;
if(l2 == NULL)return 0;
return l1->val > l2->val;
//这里比较的是优先级,默认优先级排序是“<”号,若 l1Val > l2Val 返回真,即表示l1优先级比l2小,l2先入队
//队列的top()函数指的就是优先级最高的元素,即队头元素
}
};
class Solution{
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
int len = lists.size();
if(len == 0)return NULL;
priority_queue<ListNode*,vector<ListNode*>,compare> Q;//调用小顶堆的方法构造队列!!!
for(int i = 0;i < len;i++)
{
if(lists[i])Q.push(lists[i]);
}
ListNode *head = new ListNode(0);
ListNode *tail = head;
while(!Q.empty() && Q.top() != NULL)
{
ListNode *tmp = Q.top();
Q.pop();
tail->next = tmp;
tail = tail->next;
Q.push(tmp->next);
}
return head->next;
}
};
两两交换链表中的节点
题目描述
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
示例:
给定 1->2->3->4, 你应该返回 2->1->4->3.
说明:
-
你的算法只能使用常数的额外空间。
-
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
解法
指针 p, q 分别指示链表的前后两个结点,利用指针 t 临时保存 q 的下一个结点地址。交换 p, q 指向。
注意链表为空或者链表个数为奇数的情况,做特殊判断。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode pre = head.next;
ListNode p = head;
ListNode q = head.next;
while (q != null) {
ListNode t = q.next;
q.next = p;
if (t == null || t.next == null) {
p.next = t;
break;
}
p.next = t.next;
p = t;
q = p.next;
}
return pre;
}
}
k个一组翻转链表
题目描述
给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么将最后剩余节点保持原有顺序。
示例 :
给定这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
说明 :
你的算法只能使用常数的额外空间。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换
解法
-
在 head 节点前增加一个头节点 reNode 使所有的翻转操作情况一致。
-
维护一个 num 计数,指针 pNode 从 head 节点开始,每经过 k 个节点,进行一次 k 个节点的翻转
-
将翻转后的 k 个节点与前后组的节点相连
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if(head == null || k < 2) {
return head;
}
int num = 0;
ListNode pNode = head;
ListNode lastNode = new ListNode(0);
ListNode reNode = lastNode;
lastNode.next = head;
while (pNode != null) {
num++;
if(num >= k) {
num = 0;
ListNode tempNode = pNode.next;
reverse(lastNode.next, k);
// k 个节点的尾节点指向下一组的头节点
lastNode.next.next = tempNode;
// 上一组的尾节点指向当前 k 个节点的头节点
tempNode = lastNode.next;
lastNode.next = pNode;
lastNode = tempNode;
pNode = lastNode.next;
}
else {
pNode = pNode.next;
}
}
return reNode.next;
}
private ListNode reverse(ListNode node, int i) {
if(i <= 1 || node.next == null) {
return node;
}
ListNode lastNode = reverse(node.next, i - 1);
lastNode.next = node;
return node;
}
}
删除排序数组中的重复项
题目描述
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定数组 nums = [1,1,2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,2,2,3,3,4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
解法
-
维护 i 和 j 两个指针,i 从左向右遍历数组, j 指针指向当前完成去除重复元素的最后一个值。
-
通过比较 nums[i] 与 nums[j] 的值判断 i 指向的元素是否为前一个元素的重复,若是,进入步骤3,否则,重复步骤2;
-
j 向左移动,将 nums[i] 拷贝至 nums[j] 成为新的末尾元素。
class Solution {
public int removeDuplicates(int[] nums) {
if(nums == null || nums.length == 0) {
return 0;
}
int j = 0;
for(int i = 1; i < nums.length; i++) {
if(nums[i] != nums[j]) {
nums[++j] = nums[i];
}
}
return j + 1;
}
}
移除元素
题目描述
给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
给定 nums = [3,2,2,3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,1,2,2,3,0,4,2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
注意这五个元素可为任意顺序。
你不需要考虑数组中超出新长度后面的元素。
解法
-
维护 i 和 end 两个指针,end 指向数组尾部,i 从左向右遍历数组,
-
若 nums[i] == val, 则把数组尾部的值 nums[end] 拷贝至 i 的位置,然后将 end 指针向左移动;否则,i 向右移动,继续遍历数组。
-
这样当两个 i 与 end 相遇时,end 左边的所以 val 元素都被 end 右边的非 val 元素替换。
class Solution {
public int removeElement(int[] nums, int val) {
if(nums == null || nums.length == 0) {
return 0;
}
int end = nums.length - 1;
int i = 0;
while(i <= end) {
if(nums[i] == val) {
nums[i] = nums[end];
end--;
}
else {
i++;
}
}
return end + 1;
}
}
CPP
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int len = nums.size();
if(len < 1)return 0;
auto iter = find(nums.begin(),nums.end(),val);
while(iter != nums.end())
{
nums.erase(iter);
iter = find(nums.begin(),nums.end(),val);
}
len = nums.size();
return len;
}
};
--------------------------------------------------
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int len = nums.size();
if(len < 1)return 0;
int i = 0;
while(i < len)
{
if(nums[i] == val){
nums[i] = nums[len - 1];
len--;
}
else i++;
}
return len;
}
};
实现strStr()
题目描述
实现 strStr() 函数。
给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
示例 1:
输入: haystack = "hello", needle = "ll"
输出: 2
示例 2:
输入: haystack = "aaaaa", needle = "bba"
输出: -1
说明:
当 needle
是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。
对于本题而言,当 needle
是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf()
定义相符。
解法
遍历 haystack
和 needle
,利用指针 p
, q
分别指向这两个字符串。对于每一个位置对于的字符,如果两字符相等,继续判断下一个位置的字符是否相等;否则 q
置为 0,p
置为最初匹配的字符的下一个位置,即 p - q + 1
。
class Solution {
public int strStr(String haystack, String needle) {
if ("".equals(needle)) {
return 0;
}
int len1 = haystack.length();
int len2 = needle.length();
int p = 0;
int q = 0;
while (p < len1) {
if (haystack.charAt(p) == needle.charAt(q)) {
if (len2 == 1) {
return p;
}
++p;
++q;
} else {
p -= q - 1;
q = 0;
}
if (q == len2) {
return p - q;
}
}
return -1;
}
}
两数相除
题目描述
给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。
返回被除数 dividend 除以除数 divisor 得到的商。
示例 1:
输入: dividend = 10, divisor = 3
输出: 3
示例 2:
输入: dividend = 7, divisor = -3
输出: -2
说明:
被除数和除数均为 32 位有符号整数。
除数不为 0。
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−2^31 , 2^31 − 1]。
本题中,如果除法结果溢出,则返回 2^31 − 1。
解法
-
考虑用位运算来代替乘除,用二进制表示商,则只要确定了每一个二进制位,则把这些位加和即可得到商;
-
对除数进行移位,找到最高位,然后从高到低依次比较每一位对应的数与除数的乘积,若大于则说明商的该位为1,否则为0;
class Solution {
public int divide(int dividend, int divisor) {
if(dividend == 0 || divisor == 1) {
return dividend;
}
if(divisor == 0 || (dividend == Integer.MIN_VALUE && divisor == -1)) {
return Integer.MAX_VALUE;
}
// 商的符号,true 为正,false 为负
boolean flag = true;
if((dividend < 0 && divisor > 0) || (dividend > 0 && divisor < 0)) {
flag = false;
}
long dividendLong = Math.abs((long)dividend);
long divisorLong = Math.abs((long)divisor);
int re = 0;
long factor = 0x1;
while (dividendLong >= (divisorLong << 1)) {
divisorLong <<= 1;
factor <<= 1;
}
while (factor > 0 && dividendLong > 0) {
if(dividendLong >= divisorLong) {
dividendLong -= divisorLong;
re += factor;
}
factor >>>= 1;
divisorLong >>>= 1;
}
return flag ? re : -re;
}
}
与所有单词相关联的字串
题目描述
给定一个字符串 s 和一些长度相同的单词 words。在 s 中找出可以恰好串联 words 中所有单词的子串的起始位置。
注意子串要与 words 中的单词完全匹配,中间不能有其他字符,但不需要考虑 words 中单词串联的顺序。
示例 1:
输入:
s = "barfoothefoobarman",
words = ["foo","bar"]
输出: [0,9]
解释: 从索引 0 和 9 开始的子串分别是 "barfoor" 和 "foobar" 。
输出的顺序不重要, [9,0] 也是有效答案。
示例 2:
输入:
s = "wordgoodstudentgoodword",
words = ["word","good","good"]
(ps:原题的例子为 words = ["word","student"] 和题目描述不符,这里私自改了一下)
输出: []
解法
-
用 HashMap< 单词, 出现次数 > map 来存储所有单词;
-
设单词数量为 N ,每个单词长度为 len,则我们只需要对比到 str.length() - N * len ,
再往后因为不足 N * len 个字母,肯定不匹配;
- 每次从 str 中选取连续的 N * len 个字母进行匹配时,从后向前匹配,因为若后面的单词不匹配,
无论前面的单词是否匹配,当前选取的字串一定不匹配,且,最后一个匹配的单词前的部分一定不在匹配的字串中,
这样下一次选取长度为 N * len 的字串时,可以从上次匹配比较中最后一个匹配的单词开始,减少了比较的次数;
- 考虑到要点 3 中对前一次匹配结果的利用,遍历 str 时,采用间隔为 len 的形式。
例如示例 1 ,遍历顺序为:(0 3 6 9 12 15) (1 4 7 10 13)(2 5 8 11 14)
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> re = new ArrayList<>();
if(s == null || words == null || words.length == 0 || words[0] == null) {
return re;
}
if(s.length() == 0 || words[0].length() == 0 || s.length() < words.length * words[0].length()) {
return re;
}
// 用< 单词,出现次数 > 来存储 words 中的元素,方便查找
HashMap<String,Integer> map = new HashMap();
for (String string : words) {
map.put(string, map.getOrDefault(string,0) + 1);
}
int len = words[0].length();
int strLen = s.length();
int lastStart = len * words.length - len;
for (int i = 0; i < len; i++) {
for (int j = i; j <= strLen - len - lastStart; j += len) {
String tempStr = s.substring(j, j + len);
if(map.containsKey(tempStr)) {
HashMap<String,Integer> searched = new HashMap<>();
// 从后向前依次对比
int tempIndex = j + lastStart;
String matchedStr = s.substring(tempIndex, tempIndex + len);
while (tempIndex >= j && map.containsKey(matchedStr)) {
// 正确匹配到单词
if(searched.getOrDefault(matchedStr,0) < map.get(matchedStr)) {
searched.put(matchedStr, searched.getOrDefault(matchedStr,0) + 1);
}
else {
break;
}
tempIndex -= len;
if(tempIndex < j) {
break;
}
matchedStr = s.substring(tempIndex, tempIndex + len);
}
// 完全匹配所以单词
if(j > tempIndex) {
re.add(j);
}
// 从tempIndex 到 tempIndex + len 这个单词不能正确匹配
else {
j = tempIndex;
}
}
}
}
return re;
}
}
下一个排列
题目描述
实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。
如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。
必须原地修改,只允许使用额外常数空间。
以下是一些例子,输入位于左侧列,其相应输出位于右侧列。
1,2,3
→ 1,3,2
3,2,1
→ 1,2,3
1,1,5
→ 1,5,1
解法
从后往前,找到第一个升序状态的位置,记为 i-1,在[i, length - 1] 中找到比 nums[i - 1] 大的,且差值最小的元素(如果有多个差值最小且相同的元素,取后者),进行交换。将后面的数组序列升序排列,保存。然后恢复 i,继续循环。
class Solution {
public void nextPermutation(int[] nums) {
boolean flag = false;
for (int i = nums.length - 2; i >= 0; --i) {
if (nums[i] < nums[i + 1]) {
int index = findMinIndex(nums, i, nums[i]);
swap(nums, i, index);
reverse(nums, i + 1);
flag = true;
break;
}
}
if (!flag) {
Arrays.sort(nums);
}
}
private void reverse(int[] nums, int start) {
int end = nums.length - 1;
while (start < end) {
swap(nums, start++, end--);
}
}
/**
* 找出从start开始的比val大的最小元素的下标,如果有多个,选择后者
*
* @param name
* @param start
* @param val
* @return index
*/
private int findMinIndex(int[] nums, int start, int val) {
int end = nums.length - 1;
int i = start;
for (; i < end; ++i) {
if (nums[i + 1] <= val) {
break;
}
}
return i;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
最长有效括号
题目描述
给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。
示例 1:
输入: "(()"
输出: 2
解释: 最长有效括号子串为 "()"
示例 2:
输入: ")()())"
输出: 4
解释: 最长有效括号子串为 "()()"
解法
此题采用动态规划方法。开辟一个数组空间 res,res[i] 表示必须以 s[i] 结尾的字符串的最长有效括号长度。
-
若 s[i] == '(',res[i] = 0;
-
若 s[i] == ')' && s[i - 1] == '(',res[i] = res[i - 2] + 2;
-
若 s[i] == ')' && s[i - 1] == ')',判断 s[i - 1 - res[i - 1]] 的符号,若为 '(',则 res[i] = res[i - 1] + 2 + res[i - res[i - 1] - 2]。
注意数组下标越界检查。
class Solution {
public int longestValidParentheses(String s) {
if (s == null || s.length() < 2) {
return 0;
}
char[] chars = s.toCharArray();
int n = chars.length;
int[] res = new int[n];
res[0] = 0;
res[1] = chars[1] == ')' && chars[0] == '(' ? 2 : 0;
int max = res[1];
for (int i = 2; i < n; ++i) {
if (chars[i] == ')') {
if (chars[i - 1] == '(') {
res[i] = res[i - 2] + 2;
} else {
int index = i - res[i - 1] - 1;
if (index >= 0 && chars[index] == '(') {
// ()(())
res[i] = res[i - 1] + 2 + (index - 1 >= 0 ? res[index - 1] : 0);
}
}
}
max = Math.max(max, res[i]);
}
return max;
}
}
搜索旋转排序数组
题目描述
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。
示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
示例 2:
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
思路:
因为是排序数组,而且要求是log2(n)时间搜索,所以优先选择用二分搜索法,但是这道题的难点就是不知道原数组的旋转位置在哪里,无法从中间值对比过程中直接进行二分搜索
我们还是用题目中给的例子来分析,对于数组[0 1 2 4 5 6 7] 共有下列七种旋转方法:
0 1 2 4 5 6 7
7 0 1 2 4 5 6
6 7 0 1 2 4 5
5 6 7 0 1 2 4
4 5 6 7 0 1 2
2 4 5 6 7 0 1
1 2 4 5 6 7 0
二分搜索法的关键点在于,在与中间值进行对比后,大于则在右半部分搜索,小于则在左半部分搜索,反复进行达到收敛。
我们观察上面加粗的部分,如果:
中间值比最右值小,则右半部有序递增
中间值比最右值大,则左半部有序递增
通过这个特点,对该数组进行二分搜索
class Solution {
public:
int search(vector<int>& nums, int target) {
int len = nums.size();
int left = 0;
int right = len - 1;
int mid;
while(left <= right) {
mid = (left + right) / 2;
if(nums[mid] == target)return mid;
if(nums[mid] < nums[right]) {
if(nums[right] >= target && nums[mid] < target)left = mid + 1;
else right = mid - 1;
}
else {
if(nums[left] <= target && nums[mid] > target)right = mid - 1;
else left = mid + 1;
}
}
return -1;
}
};
我发现其实这种暴力枚举,时间差不了多少,各位赶时间就直接暴力吧!!!
class Solution {
public:
int search(vector<int>& nums, int target) {
int len = nums.size();
for(int i = 0;i<len;i++) {
if(target == nums[i])return i;
}
return -1;
}
};
在排序数组中查找元素的第一个和最后一个位置
问题描述
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
思路
二分查找找下标,找到下标对其左右查找是否等于目标值就好了
class Solution {
public:
bool binarySearch(vector<int> &nums,int &target,int &pos,int left,int right){
int mid = left + ((right - left) >> 1);
if(nums[left] == target){
pos = left;
return true;
}
if(nums[right] == target){
pos = right;
return true;
}
if(nums[mid] == target){
pos = mid;
return true;
}
if(left == mid){
return false;
}
else if(nums[mid] > target){
return binarySearch(nums,target,pos,left,mid-1);
}
else if(nums[mid] < target){
return binarySearch(nums,target,pos,mid+1,right);
}
return false;
}
vector<int> searchRange(vector<int>& nums, int target) {
int pos = 0;
int len = nums.size();
if(len == 0)return {-1,-1};
if(binarySearch(nums,target,pos,0,len-1)){
int left = pos;
int right = pos;
while(left>0 && nums[left-1] == nums[pos])left--;
while(right<len-1 && nums[right+1] == nums[pos])right++;
return {left,right};
}
else{
return {-1,-1};
}
}
};
搜索位置描述
题目描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0
解法
首先判断传入的数组为 0,1 这样的长度。
因为是一个给定的排序数组,在循环时就可以判断是否存在的同时判断大小,有相同的则直接返回索引,
不存在则判断大小,只要相较于当前索引的元素较小,则可以认为该目标数在数组中无对应元素,直接返回索引即可。
除此之外还可用二分法做解。
class Solution {
public int searchInsert(int[] nums, int target) {
if(nums.length == 0) {
return 0;
}
if(nums.length == 1) {
if(nums[0] < target) {
return 1;
} else {
return 0;
}
}
for(int i = 0;i < nums.length;i++) {
if(nums[i] == target) {
return i;
} else {
int s = Math.min(nums[i],target);
if(s == target) {
return i;
}
}
}
return nums.length;
}
}
- 二分法
class Solution {
public int searchInsert(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return 0;
}
int low = 0;
int high = nums.length - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (nums[mid] == target) {
return mid;
}
if (nums[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return low;
}
}
CPP
思路1:
-
先调函数查找是否存在target元素
-
若存在,用二分法进行查找,或者顺序遍历
-
若不存在,则顺序遍历插入
时间复杂度O(log2(n))~O(n)
思路2:
- 直接顺序遍历---需要点取巧,下标比nums长度小,nums[p]元素要比targat小
时间复杂度O(n)
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int len = nums.size();
if(len == 0){
nums.push_back(target);
return 0;
}
auto iter = find(nums.begin(),nums.end(),target);
if(iter != nums.end()){
return binarySearch(nums,0,len - 1,target);
}
else{
int slow = 0;
int fast = 1;
if(nums[0] >= target)return 0;
if(nums[len-1] <= target)return len;
while(fast < len){
if(nums[slow] <= target && nums[fast] > target){
nums.insert(nums.begin() + fast,target);
return fast;
}
else{
slow++;
fast++;
}
}
return fast;
}
}
int binarySearch(vector<int> &nums,int left,int right,int target){
if(nums[left] == target)return left;
if(nums[right] == target)return right;
int mid = (left + right) / 2;
if(nums[mid] > target)return binarySearch(nums,left+1,mid,target);
else if(nums[mid] < target)return binarySearch(nums,mid,right-1,target);
else return mid;
}
};
-------------------------
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
if (nums.size() == 0) {
nums.push_back(target);
return 0;
}
unsigned int i = 0;
while(i<nums.size()&&nums[i]<target)i++;
nums.insert(nums.begin() + i, target);
return i;
}
};
报数
题目描述
报数序列是一个整数序列,按照其中的整数的顺序进行报数,得到下一个数。其前五项如下:
1. 1
2. 11
3. 21
4. 1211
5. 111221
1
被读作 "one 1"
("一个一"
) , 即 11
。
11
被读作 "two 1s"
("两个一"
), 即 21
。
21
被读作 "one 2"
, "one 1"
("一个二"
, "一个一"
) , 即 1211
。
给定一个正整数 n(1 ≤ n ≤ 30),输出报数序列的第 n 项。
注意:整数顺序将表示为一个字符串。
示例 1:
输入: 1
输出: "1"
示例 2:
输入: 4
输出: "1211"
组合总和
问题描述
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
示例 2:
输入: candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
思路
这种题肯定是用回溯递归的,和46题全排列那道题很像
[1,2,3,4]构建成回溯树如下状态,一次循环开始进入一个数,一次循环后pop出来一个数,形成一种对称性回溯
1
/ | \
12 13 14
/ |
123 124 .....
CPP
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
int len = candidates.size();
vector<vector<int>> ans;
vector<int> tmp;
sort(candidates.begin(),candidates.end());
dfs(ans,tmp,candidates,target,len,0);
return ans;
}
void dfs(vector<vector<int>> &ans,vector<int> &tmp,vector<int> &nums,int target,int len,int index){
if(target == 0) ans.push_back(tmp);
for(int i = index;i<len && target >= nums[i];i++){
tmp.push_back(nums[i]);
dfs(ans,tmp,nums,target-nums[i],len,i);
tmp.pop_back();
}
}
};
组合总和2
题目描述
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
思路
和39题一模一样,注意他有重复数,需要去除重复的结果.
还要注意回溯是往后回溯,不是原地回溯了
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
vector<vector<int>> ans;
vector<int> tmp;
sort(candidates.begin(),candidates.end());
int len = candidates.size();
dfs(ans,tmp,candidates,target,len,0);
return ans;
}
void dfs(vector<vector<int>> &ans,vector<int> &tmp,vector<int> &nums,int target,int len,int index) {
if(target == 0){
auto iter = find(ans.begin(),ans.end(),tmp);
if(iter == ans.end())ans.push_back(tmp);
}
for(int i = index;i<len && target >= nums[i];i++){
tmp.push_back(nums[i]);
dfs(ans,tmp,nums,target - nums[i],len,i+1);//注意i+1
tmp.pop_back();
}
}
};
缺失的第一个正数
问题描述
给定一个未排序的整数数组,找出其中没有出现的最小的正整数。
示例 1:
输入: [1,2,0]
输出: 3
示例 2:
输入: [3,4,-1,1]
输出: 2
示例 3:
输入: [7,8,9,11,12]
输出: 1
说明:
你的算法的时间复杂度应为O(n),并且只能使用常数级别的空间。
思路
题目的描述一看有点不好理解,其实是把它们排序后,[-1,1,2,4,4,5,6]这里面缺的第一个正整数是3,0不算正整数
-
对数组排序
-
过滤小于等于0的部分
-
从1开始比较,注意过滤重复的元素
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
sort(nums.begin(),nums.end());
int len = nums.size();
if(len == 0)return 1;
int i = 0;
while(nums[i] <= 0 && i < len)i++;
if(i == len)return 1;
int tmp = 1;
while(i<len){
if(nums[i] != tmp)return tmp;
while(len>i+1 && nums[i] == nums[i+1])i++;//去重
i++;
tmp++;
}
return tmp;
}
};
接雨水
问题描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组[0,1,0,2,1,0,1,3,2,1,2,1]
表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 感谢 Marcos 贡献此图。
示例:
输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
思路
方法是找凹槽,怎么找呢?
-
设置
slow,fast
两个下标代表凹槽的左右边界,一旦遇到height[fast]>=height[slow]
的情况,计算凹槽的容积 -
上面情况是以右边界高度一定大于左边界为准的,当形成凹槽且左边界大于右边界时,要怎么记录呢?答案是设置
stopPoint
点,规则是当height[fast]>height[stopPoint]
时有stopPoint = fast
记录右边最高点;同时当fast越界时,会到stopPoint
上
class Solution {
public:
int trap(vector<int>& height) {
int len = height.size();
if(len == 0 || len == 1)return 0;
int slow = 0;
int fast = 1;
int stopPoint;
int total = 0;
int bottom;
int tmp = 0;
while(fast < len){
//每次更新stopPoint
stopPoint = fast;
while(fast < len && height[fast] <= height[slow]){
if(height[fast] > height[stopPoint])
stopPoint = fast;
fast++;
}
//越界了要回到stopPoint
if(fast >= len)fast = stopPoint;
tmp = 0;
bottom = min(height[slow],height[fast]);
for(int i = slow+1;i<fast;i++){
tmp += bottom - height[i];
}
slow = fast;
total += tmp;
fast++;
}
return total;
}
};
全排列
题目描述
给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
解法
将数组的首元素依次与数组的每个元素交换,对于每一轮交换,对后面的数组进行递归调用。当元素只剩下一个时,添加此时的数组到 list 中。
class Solution {
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
permute(list, nums, 0);
return list;
}
private void permute(List<List<Integer>> list, int[] nums, int start) {
int end = nums.length - 1;
if (start == end) {
list.add(Arrays.stream(nums).boxed().collect(Collectors.toList()));
return;
}
for (int i = start; i <= end; ++i) {
swap(nums, i, start);
permute(list, nums, start + 1);
swap(nums, i, start);
}
}
private static void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
全排列 II
题目描述
给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
解法
解法①:
将数组的首元素依次与数组的每个元素交换(两元素不相等才进行交换),对于每一轮交换,对后面的数组进行递归调用。当元素只剩下一个时,添加此时的数组到 list 中。
注意:第 i 个数字与第 j 个数字交换时,要求[i, j) 中没有与第 j 个数字相等的数。
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
permute(list, nums, 0);
return list;
}
private void permute(List<List<Integer>> list, int[] nums, int start) {
int end = nums.length - 1;
if (start == end) {
List<Integer> tmp = new ArrayList<>();
for (int val : nums) {
tmp.add(val);
}
list.add(tmp);
}
for (int i = start; i <= end; ++i) {
if (isSwap(nums, start, i)) {
swap(nums, i, start);
permute(list, nums, start + 1);
swap(nums, i, start);
}
}
}
private boolean isSwap(int[] nums, int from, int to) {
for (int i = from; i < to; ++i) {
if (nums[i] == nums[to]) {
// [from, to) 中出现与 第 to 个数相等的数,返回 false,不进行交换和全排列操作
return false;
}
}
return true;
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
解法②:
利用空间换取时间,减少 n^2 复杂度。这里的空间,可以采用数组,或者 HashMap。
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
permute(list, nums, 0);
return list;
}
private void permute(List<List<Integer>> list, int[] nums, int start) {
int end = nums.length - 1;
if (start == end) {
List<Integer> tmp = new ArrayList<>();
for (int val : nums) {
tmp.add(val);
}
list.add(tmp);
}
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i <= end; ++i) {
max = Math.max(max, nums[i]);
min = Math.min(min, nums[i]);
}
// 空间换取时间
boolean[] flag = new boolean[max - min + 1];
for (int i = start; i <= end; ++i) {
int index = nums[i] - min;
if (!flag[index]) {
// 说明当前的数没在之前出现过
swap(nums, i, start);
permute(list, nums, start + 1);
swap(nums, i, start);
// 标志位设为 true
flag[index] = true;
}
}
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
解法➂:
这是非递归的解法。思路就是:
-
先保存升序数组;
-
找到下一个比该数组大的数组,保存;
-
直到数组呈降序。
如何找到下一个数组呢?从后往前,找到第一个升序状态的位置,记为 i-1,在[i, length - 1] 中找到比 nums[i - 1] 大的,且差值最小的元素(如果有多个差值最小且相同的元素,取后者),进行交换。将后面的数组序列升序排列,保存。然后恢复 i,继续循环。
class Solution {
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
// 先排序,并添加至 list 中
Arrays.sort(nums);
addList(list, nums);
permute(list, nums);
return list;
}
private void permute(List<List<Integer>> list, int[] nums) {
int n = nums.length;
for (int i = n - 1; i > 0; --i) {
if (nums[i - 1] < nums[i]) {
// 在数组右边找到比 nums[i - 1] 大的,但差值最小的数的下标
// 若有多个最小差值,取后者
int index = findMinIndex(nums, i, nums[i - 1]);
swap(nums, i - 1, index);
sortArr(nums, i);
addList(list, nums);
i = n;
}
}
}
private void sortArr(int[] nums, int start) {
int n = nums.length - 1;
while (start < n) {
swap(nums, start, n);
++start;
--n;
}
}
private int findMinIndex(int[] nums, int start, int val) {
int index = start;
int distance = nums[start] - val;
for (int i = start; i < nums.length; ++i) {
if (nums[i] > val && (nums[i] - val <= distance)) {
distance = nums[i] - val;
index = i;
}
}
return index;
}
private void addList(List<List<Integer>> list, int[] nums) {
List<Integer> tmpList = new ArrayList<>();
for (int val : nums) {
tmpList.add(val);
}
list.add(tmpList);
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
48旋转图像
问题描述
给定一个 n × n 的二维矩阵表示一个图像。
将图像顺时针旋转 90 度。
说明:
你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
示例 1:
给定 matrix =
[
[1,2,3],
[4,5,6],
[7,8,9]
],
原地旋转输入矩阵,使其变为:
[
[7,4,1],
[8,5,2],
[9,6,3]
]
示例 2:
给定 matrix =
[
[ 5, 1, 9,11],
[ 2, 4, 8,10],
[13, 3, 6, 7],
[15,14,12,16]
],
原地旋转输入矩阵,使其变为:
[
[15,13, 2, 5],
[14, 3, 4, 1],
[12, 6, 8, 9],
[16, 7,10,11]
]
思路:
本来以为是矩阵坐标表换的一种,用初等行变换做,但是这里和矩阵坐标没任何关系,而是整个矩阵旋转,所以老实找规律
1 2 3 顺90° 7 4 1
4 5 6 ========> 8 5 2
7 8 9 9 6 3
等价于
1 2 3 转置 1 4 7 左右互换 7 4 1
4 5 6 ========> 2 5 8 ===========> 8 5 2
7 8 9 3 6 9 9 6 3
先当做是一种规律,数学证明以后补
-
先将矩阵转置
-
左右各列对称互换
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
if(n <= 1)return ;
//先做转置
for(int i = 0 ; i < n ; i++){
for(int j = i;j < n ;j++){
swap(matrix[i][j],matrix[j][i]);
}
}
//再做水平互换
for(int i = 0 ; i < n ; i++){
for(int j = 0;j < n/2;j++){
swap(matrix[i][j],matrix[i][n-1-j]);
}
}
}
};
最大子序和
题目描述
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
解法
此题可以用动态规划法,开辟一个数组res,res[i] 表示以当前结点nums[i] 结尾的最大连续子数组的和。最后计算 res 的最大元素即可。
也可以用分治法,最大连续子数组有三种情况:在原数组左侧、右侧、跨中间结点,返回这三者的最大值即可。
动态规划法:
class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
if (n == 1) {
return nums[0];
}
int[] res = new int[n];
res[0] = nums[0];
int max = res[0];
for (int i = 1; i < n; ++i) {
res[i] = Math.max(res[i - 1] + nums[i], nums[i]);
max = Math.max(res[i], max);
}
return max;
}
}
分治法:
class Solution {
public int maxSubArray(int[] nums) {
return maxSubArray(nums, 0, nums.length - 1);
}
private int maxSubArray(int[] nums, int start, int end) {
if (start == end) {
return nums[start];
}
int mid = start + ((end - start) >> 1);
int left = maxSubArray(nums, start, mid);
int right = maxSubArray(nums, mid + 1, end);
int leftSum = 0;
int leftMax = Integer.MIN_VALUE;
for (int i = mid; i >= start; --i) {
leftSum += nums[i];
leftMax = Math.max(leftSum, leftMax);
}
int rightSum = 0;
int rightMax = Integer.MIN_VALUE;
for (int i = mid + 1; i <= end; ++i) {
rightSum += nums[i];
rightMax = Math.max(rightSum, rightMax);
}
return Math.max(Math.max(left, right), leftMax + rightMax);
}
}
螺旋矩阵
题目描述
给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。
示例 1:
输入:
[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
[ 7, 8, 9 ]
]
输出: [1,2,3,6,9,8,7,4,5]
示例 2:
输入:
[
[1, 2, 3, 4],
[5, 6, 7, 8],
[9,10,11,12]
]
输出: [1,2,3,4,8,12,11,10,9,5,6,7]
解法
由外往里,一圈圈遍历矩阵即可。遍历时,如果只有 1 行或者 1 列。直接遍历添加这一行/列元素。否则遍历一圈,陆续添加元素。
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
if (matrix == null || matrix.length == 0) {
return new ArrayList<Integer>();
}
int m = matrix.length;
int n = matrix[0].length;
int m1 = 0;
int n1 = 0;
int m2 = m - 1;
int n2 = n - 1;
List<Integer> res = new ArrayList<>();
while (m1 <= m2 && n1 <= n2) {
goCircle(res, matrix, m1++, n1++, m2--, n2--);
}
return res;
}
private void goCircle(List<Integer> res, int[][] matrix, int m1, int n1, int m2, int n2) {
if (m1 == m2) {
for (int j = n1; j <= n2; ++j) {
res.add(matrix[m1][j]);
}
} else if (n1 == n2) {
for (int i = m1; i <= m2; ++i) {
res.add(matrix[i][n1]);
}
} else {
for (int j = n1; j < n2; ++j) {
res.add(matrix[m1][j]);
}
for (int i = m1; i < m2; ++i) {
res.add(matrix[i][n2]);
}
for (int j = n2; j > n1; --j) {
res.add(matrix[m2][j]);
}
for (int i = m2; i > m1; --i) {
res.add(matrix[i][n1]);
}
}
}
}
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
输入: [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入: [[1,4],[4,5]]
输出: [[1,5]]
解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
思路:
-
对容器按start值从小到大排序
-
两两顺序比较,用第一个元素的end值和第二个元素的start值比较
-
如果后start比前end小,且前end比后end小,则合并!
-
不满足3,则直接插入
时间复杂度O(n)
/**
* Definition for an interval.
* struct Interval {
* int start;
* int end;
* Interval() : start(0), end(0) {}
* Interval(int s, int e) : start(s), end(e) {}
* };
*/
bool cmp(Interval &val1,Interval &val2){
return !(val1.start >= val2.start);
}
class Solution {
public:
vector<Interval> merge(vector<Interval>& intervals) {
int len = intervals.size();
if(len <= 1)return intervals;
sort(intervals.begin(),intervals.end(),cmp);
vector<Interval> ans;
ans.push_back(intervals[0]);
for(int i = 1;i<len;i++){
if(ans.back().end >= intervals[i].start){
ans.back().end = max(ans.back().end,intervals[i].end);
}
else{
ans.push_back(intervals[i]);
}
}
return ans;
}
};
最后一个单词的长度
题目描述
给定一个仅包含大小写字母和空格 ' '
的字符串,返回其最后一个单词的长度。
如果不存在最后一个单词,请返回 0
。
说明:一个单词是指由字母组成,但不包含任何空格的字符串。
示例:
输入: "Hello World"
输出: 5
螺旋矩阵 II
题目描述
给定一个正整数 n,生成一个包含 1 到 n² 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。
示例:
输入: 3
输出:
[
[ 1, 2, 3 ],
[ 8, 9, 4 ],
[ 7, 6, 5 ]
]
解法
定义一个变量 val
,由外往里,一圈圈遍历矩阵,进行赋值,每次赋值后 val++
。
class Solution {
public int[][] generateMatrix(int n) {
if (n < 1) {
return null;
}
int[][] res = new int[n][n];
int val = 1;
int m1 = 0;
int m2 = n - 1;
while (m1 < m2) {
for (int j = m1; j < m2; ++j) {
res[m1][j] = val++;
}
for (int i = m1; i < m2; ++i) {
res[i][m2] = val++;
}
for (int j = m2; j > m1; --j) {
res[m2][j] = val++;
}
for (int i = m2; i > m1; --i) {
res[i][m1] = val++;
}
++m1;
--m2;
}
if (m1 == m2) {
res[m1][m1] = val;
}
return res;
}
}
旋转链表
题目描述
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
示例 1:
输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL
示例 2:
输入: 0->1->2->NULL, k = 4
输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL
向右旋转 2 步: 1->2->0->NULL
向右旋转 3 步: 0->1->2->NULL
向右旋转 4 步: 2->0->1->NULL
解法
利用双指针p
,q
分别指向链表的头部和尾部,题目是右移 k 个位置,右移时,q
需要指向q
的前一个位置,似乎不太好做。换种思路,改用左移,右移 k 位相当于左移 len-k 位。循环移位即可。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if (head == null) {
return head;
}
int len = 1;
ListNode p = head;
ListNode q = head;
ListNode t = p.next;
while (q.next != null) {
++len;
q = q.next;
}
if (len == 1 || k % len == 0) {
return head;
}
k %= len;
// 右移 k 个位置,相当于左移 (len-k) 个位置
k = len - k;
for (int i = 0; i < k; ++i) {
q.next = p;
p.next = null;
q = q.next;
p = t;
t = p.next;
}
return p;
}
}
不同路径
题目描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
例如,上图是一个 7 x 3 的网格。有多少可能的路径?
说明:m 和 n 的值均不超过 100。
示例 1:
输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右
示例 2:
输入: m = 7, n = 3
输出: 28
解法
在网格中,最左侧和最上方每个格子均只有一条可能的路径能到达。而其它格子,是它“左方格子的路径数+上方格子的路径数之和”(递推式)。开辟一个二维数组存放中间结果。
class Solution {
public int uniquePaths(int m, int n) {
int[][] res = new int[n][m];
for (int i = 0; i < m; ++i) {
res[0][i] = 1;
}
for (int i = 1; i < n; ++i) {
res[i][0] = 1;
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j < m; ++j) {
res[i][j] = res[i - 1][j] + res[i][j - 1];
}
}
return res[n - 1][m - 1];
}
}
不同路径 II
题目描述
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
说明:m 和 n 的值均不超过 100。
示例 1:
输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
解法
与上题相比,这一题仅仅多了一个条件,就是网格中存在障碍物。对于最左侧和最上方,如果存在障碍物,那么往右或者往下的网格中路径均为 0。而其它格子,如果该格子有障碍物,那么路径为 0,否则是它“左方格子的路径数+上方格子的路径数之和”(递推式)。同样开辟一个二维数组存放中间结果。
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int n = obstacleGrid.length;
int m = obstacleGrid[0].length;
int[][] res = new int[n][m];
int i = 0;
while (i < n && obstacleGrid[i][0] == 0) {
// 无障碍物
res[i++][0] = 1;
}
while (i < n) {
res[i++][0] = 0;
}
i = 0;
while (i < m && obstacleGrid[0][i] == 0) {
// 无障碍物
res[0][i++] = 1;
}
while (i < m) {
res[0][i++] = 0;
}
for (int k = 1; k < n; ++k) {
for (int j = 1; j < m; ++j) {
res[k][j] = obstacleGrid[k][j] == 1 ? 0 : (res[k - 1][j] + res[k][j - 1]);
}
}
return res[n - 1][m - 1];
}
}
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
思路:
和62题《不同路径》是同一个思路,都是动态规划,区别是这里是带权值的路径
-
创建二维数组
path[row][column]
,path[i][j] i∈[0,row-1],j∈[0,column-1]
表示到坐标(i+1,j+1)
的最短路径和 -
首行首列初始化;首行初始化是上一行最短路径和+该位置权值,对应公式
path[i][0] = path[i-1][0] + grid[i][0]; i∈[1,row-1],j∈[1,column-1]
同理首列初始化g公式为path[0][i] = path[0][i-1] + grid[0][i];
-
对各点
path[i][j]
求最短路径和,坐标(i,j)
的最短路径可以由上一行得来,或者是前一列得来,动态规划方程为:(前一列最小路径||前一行最小路径)两者较小值+当前坐标权值
,公式为:path[i][j] = min(path[i-1][j],path[i][j]) + grid[i][j];
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int row = grid.size();
if(row == 0)return 0;
int column = grid[0].size();
vector<vector<int>> path(row,vector<int>(column,0));
path[0][0] = grid[0][0];
for(int i = 1 ; i < column ; i++)path[0][i] = path[0][i-1] + grid[0][i];
for(int i = 1 ; i < row;i++)path[i][0] = path[i-1][0]+grid[i][0];
for(int i = 1;i<row;i++){
for(int j = 1;j<column;j++){
path[i][j] = min(path[i-1][j],path[i][j-1]) + grid[i][j];
}
}
return path[row-1][column-1];
}
};
加一
问题描述
给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。
最高位数字存放在数组的首位, 数组中每个元素只存储一个数字。
你可以假设除了整数 0 之外,这个整数不会以零开头。
示例 1:
输入: [1,2,3]
输出: [1,2,4]
解释: 输入数组表示数字 123。
示例 2:
输入: [4,3,2,1]
输出: [4,3,2,2]
解释: 输入数组表示数字 4321。
思路:
-
末尾加1,注意超过10的情况,要取余进1
-
前后关系式应该是
digits[i-1] = digits[i-1] + digits[i] / 10;
,即前一个元素的值应该是本身加上后一个元素的进位 -
若首元素>=10,则需要插入元素
class Solution {
public:
vector<int> plusOne(vector<int>& digits) {
int len = digits.size();
if(len == 0)return digits;
digits[len-1]++;
int num = digits[len - 1];
for(int i = len - 1;i>=1;i--){
digits[i-1] = digits[i-1] + digits[i]/10;
digits[i] %= 10;
}
if(digits[0] >= 10){
digits.insert(digits.begin(),digits[0]/10);
digits[1] = digits[1] % 10;
}
return digits;
}
};
x 的平方根
题目描述
实现 int sqrt(int x)
函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。
示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842...,
由于返回类型是整数,小数部分将被舍去。
解法
爬楼梯
题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
解法
爬上 1 阶有 1 种方法,爬上 2 阶有 2 种方法,爬上 n 阶 f(n) 有 f(n - 1) + f(n - 2) 种方法。可以利用数组记录中间结果,防止重复计算。
class Solution {
public int climbStairs(int n) {
if (n < 3) {
return n;
}
int[] res = new int[n + 1];
res[1] = 1;
res[2] = 2;
for (int i = 3; i < n + 1; ++i) {
res[i] = res[i - 1] + res[i - 2];
}
return res[n];
}
}
给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。
示例 1:
输入:
[
[1,1,1],
[1,0,1],
[1,1,1]
]
输出:
[
[1,0,1],
[0,0,0],
[1,0,1]
]
示例 2:
输入:
[
[0,1,2,0],
[3,4,5,2],
[1,3,1,5]
]
输出:
[
[0,0,0,0],
[0,4,5,0],
[0,3,1,0]
]
进阶:
一个直接的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案。
一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。
你能想出一个常数空间的解决方案吗?
思路1
-
创建行列辅助数组,先遍历矩阵,定位元素是0的行列,分别加入行数组,列数组
-
从行数组中取出元素,整行置零;同理列数组
空间复杂度O(m+n)
,而且分别进行处理和列处理时有重复操作
思路2(优化思路1)
-
先检查首行首列有没有0,有的话设置bool标记
-
从第二行第二列开始遍历,如果发现有0,设置
matrix[i][0] = 0和matrix[0][j] = 0
,即把首行首列对应行列值设置为0 -
遍历首行首列,把值为0的行按行设置为0;列同理
-
查看标记位,看是否需要把首行首列设置为0
这种思路没有用额外的空间,但是时间复杂度和思路1一样,都有待解决重复操作的问题
整体好于思路1,时间复杂度比思路1稳定
Solution1
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
if(matrix.empty())return;
//行数组,列数组
int rowNum = matrix.size();
int columnNum = matrix[0].size();
vector<int> rowVec;
vector<int> columnVec;
for(int i = 0;i<rowNum;i++){
for(int j = 0;j<columnNum;j++){
if(matrix[i][j] == 0){
auto iter = find(rowVec.begin(),rowVec.end(),i);
if(iter == rowVec.end())rowVec.push_back(i);
iter = find(columnVec.begin(),columnVec.end(),j);
if(iter == columnVec.end())columnVec.push_back(j);
}
}
}
rowNum = rowVec.size();
if(rowNum == 0)return;
int row;
int column;
//行处理
for(int i = 0;i<rowNum;i++){
row = rowVec[i];
for(int j = 0;j<columnNum;j++){
matrix[row][j] = 0;
}
}
//列处理
columnNum = columnVec.size();
rowNum = matrix.size();
for(int i = 0 ; i < columnNum;i++){
column = columnVec[i];
for(int j = 0;j<rowNum;j++){
matrix[j][column] = 0;
}
}
}
};
Solution 2
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
if(matrix.empty()) return;
int m = matrix.size();
int n = matrix[0].size();
bool row = false , column = false;
for(int i = 0; i < m; i++)//判断第1列的0;
{
if(matrix[i][0] == 0)
{
column = true;
break;
}
}
for(int i = 0; i < n; i ++)//判断第1行的0;
{
if(matrix[0][i] == 0)
{
row = true;
break;
}
}
for(int i = 1; i < m;i++)
{
for(int j = 1; j < n;j++)
{
if(matrix[i][j] == 0)
{
matrix[0][j] = 0;
matrix[i][0] = 0;
}
}
}
for(int i = 1; i < m;i++)
{
for(int j = 1; j < n;j++)
{
if(matrix[i][0] == 0 || matrix[0][j] == 0)
{
matrix[i][j] = 0;
}
}
}
if(row)
for(int i = 0; i < n;i++)
matrix[0][i] = 0;
if(column)
for(int i = 0; i < m;i++)
matrix[i][0] = 0;
return;
}
};
搜索二维矩阵
问题描述
编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
-
每行中的整数从左到右按升序排列。
-
每行的第一个整数大于前一行的最后一个整数。
示例 1:
输入:
matrix = [
[1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 50]
]
target = 3
输出: true
示例 2:
输入:
matrix = [
[1, 3, 5, 7],
[10, 11, 16, 20],
[23, 30, 34, 50]
]
target = 13
输出: false
思路
-
因为矩阵按特性排列,所以先定位行坐标
-
定位行坐标后直接调函数
一开始本来想定位到行之后用二分查找的,但是考虑到这个元素本身可能不存在,所以建议不调迭代器的话用顺序查找吧
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
if(matrix.empty())return false;
size_t row = matrix.size();
size_t column = matrix[0].size();
if(column == 0 || column == 0)return false;
if(target < matrix[0][0] || target > matrix[row-1][column-1])return false;
for(int i = 0;i<row;i++){
if(matrix[i][column-1]<target)continue;
auto iter = find(matrix[i].begin(),matrix[i].end(),target);
if(iter != matrix[i].end())return true;
else return false;
}
return false;
}
};
颜色分类
题目描述
给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
注意:
不能使用代码库中的排序函数来解决这道题。
示例:
输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]
进阶:
- 一个直观的解决方案是使用计数排序的两趟扫描算法。
首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。
- 你能想出一个仅使用常数空间的一趟扫描算法吗?
解法
指针 p 指示左侧等于 0 区域的最后一个元素,q 指示右侧等于 2 的第一个元素。p, q 初始分别指向 -1, nums.length。
cur 从 0 开始遍历:
-
若 nums[cur] == 0,则与 p 的下一个位置的元素互换,p+1, cur+1;
-
若 nums[cur] == 1,++cur;
-
若 nums[cur] == 2,则与 q 的前一个位置的元素互换,q-1,cur不变。
解法2
因为排序元素的类型有限而且不多,比较直观的方法是使用计数排序,就是创建一个包含所有类型的数组(如创建数组count[3],count[0]=k表示类型是“0”的元素有k个)
,利用下标作为统计标识,在对应下标元素上+-
-
创建一个含有3个元素的数组并初始化为0
count[3] = {0}
-
遍历nums,在
count
数组对应数字下+1 -
利用计数数组对nums重新赋值
Java(解法1)
class Solution {
public void sortColors(int[] nums) {
int p = -1;
int q = nums.length;
int cur = 0;
while (cur < q) {
if (nums[cur] == 0) {
swap(nums, cur++, ++p);
} else if (nums[cur] == 1) {
++cur;
} else {
swap(nums, --q, cur);
}
}
}
private void swap(int[] nums, int i, int j) {
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
}
CPP(解法2)
class Solution {
public:
void sortColors(vector<int>& nums) {
if(nums.empty())return ;
int count[3] = {0};
size_t len = nums.size();
for(int i = 0;i<len;i++){
count[nums[i]]++;
}
int index = 0;
for(int i = 0;i<3;i++){
while(count[i] != 0){
nums[index++] = i;
count[i]--;
}
}
}
};
删除排序数组中的重复项 II
问题描述
给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
示例 1:
给定 nums = [1,1,1,2,2,3],
函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。
你不需要考虑数组中超出新长度后面的元素。
示例 2:
给定 nums = [0,0,1,1,1,1,2,3,3],
函数应返回新长度 length = 7, 并且原数组的前五个元素被修改为 0, 0, 1, 1, 2, 3, 3 。
你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
思路[CPP]
本题对CPP而言主要考察STL,遍历+统计+迭代器指针
-
去除特殊情况(如数组空或长度为1)
-
设置统计位k=1,用迭代器指向数组第二位元素
-
遍历,让迭代器指向元素与迭代器前一位元素比较,相同则k++,不同则k=1
-
若k==3,立即删除当前元素,指针不动,仍是指向当前位置;否则执行指针后移
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
if(nums.empty())return 0;
size_t len = nums.size();
if(len == 1)return 1;
auto iter = nums.begin();
iter++;
int k = 1;
while(iter != nums.end()){
if(*iter == *(iter-1))k++;
else k = 1;
if(k==3){
nums.erase(iter);
k--;
}
else {
iter++;
}
}
len = nums.size();
return len;
}
};
删除排序链表中的重复元素 II
题目描述
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中没有重复出现的数字。
示例 1:
输入: 1->2->3->3->4->4->5
输出: 1->2->5
示例 2:
输入: 1->1->1->2->3
输出: 2->3
解法
利用链表的递归性,需要注意处理连续 n(n>=3) 个结点相等的情况。若相邻只有两个结点相等,则直接返回deleteDuplicates(head.next.next);若相邻结点超过 3 个相等,返回 deleteDuplicates(head.next)。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
if (head.val == head.next.val) {
if (head.next.next == null) {
return null;
}
if (head.val == head.next.next.val) {
return deleteDuplicates(head.next);
}
return deleteDuplicates(head.next.next);
}
head.next = deleteDuplicates(head.next);
return head;
}
}
删除排序链表中的重复元素
题目描述
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1:
输入: 1->1->2
输出: 1->2
示例 2:
输入: 1->1->2->3->3
输出: 1->2->3
解法
利用链表的递归性,先判断当前结点的值与下个结点值是否相等,是的话,链表为 deleteDuplicates(ListNode head),否则为 head->deleteDuplicates(head.next)。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
head.next = deleteDuplicates(head.next);
return head.val == head.next.val ? head.next : head;
}
}
柱状图中最大的矩形
题目描述
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]
。
图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10
个单位。
示例:
输入: [2,1,5,6,2,3]
输出: 10
解法
思路一
从前往后遍历 heightss[0...n]:
-
若 heightss[i] > heightss[i - 1],则将 i 压入栈中;
-
若 heightss[i] <= heightss[i - 1],则依次弹出栈,计算栈中能得到的最大矩形面积。
注意,压入栈中的是柱子的索引,而非柱子的高度。(通过索引可以获得高度、距离差)
class Solution {
public int largestRectangleArea(int[] heights) {
if (heights == null || heights.length == 0) {
return 0;
}
int n = heights.length;
if (n == 1) {
return heights[0];
}
// 创建一个新的数组,数组长度为 n + 1,最后一个元素值赋为 0
// 确保在后面的遍历中,原数组最后一个元素值能得到计算
int[] heightss = new int[n + 1];
heightss[n] = 0;
for (int i = 0; i < n; ++i) {
heightss[i] = heights[i];
}
Stack<Integer> stack = new Stack<>();
int max = 0;
for (int i = 0; i <= n;) {
if (stack.isEmpty() || heightss[i] > heightss[stack.peek()]) {
stack.push(i++);
} else {
int index = stack.pop();
max = Math.max(max, heightss[index] * (stack.isEmpty() ? i : i - stack.peek() - 1));
}
}
return max;
}
}
C++版实现:
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
if(heights.empty())return 0;
int len = heights.size();
stack<int> s_stack;
int ans = 0;
for(int i = 0;i < len;i++){
if(s_stack.empty() || heights[i] >= s_stack.top())
{//满足升序条件
s_stack.push(heights[i]);
}
else
{//不满足升序
int count = 0;
while(!s_stack.empty() && s_stack.top() > heights[i])
{
count++;
ans = max(ans,s_stack.top() * count);
s_stack.pop();
}
while(count > 0)
{
s_stack.push(heights[i]);
count--;
}
s_stack.push(heights[i]);
}
}
int count = 1;
while(!s_stack.empty()){
ans = max(ans,s_stack.top() * count);
s_stack.pop();
count++;
}
return ans;
}
};
分隔链表
题目描述
给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
你应当保留两个分区中每个节点的初始相对位置。
示例:
输入: head = 1->4->3->2->5->2, x = 3
输出: 1->2->2->4->3->5
解法
维护 left
, right
两个链表,遍历 head
链表,若对应元素值小于 x
,将该结点插入 left
链表中,否则插入 right
链表中。最后 right
尾部指向空,并将 left
尾部指向 right
头部,使得它们串在一起。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode partition(ListNode head, int x) {
ListNode leftDummy = new ListNode(-1);
ListNode rightDummy = new ListNode(-1);
ListNode leftCur = leftDummy;
ListNode rightCur = rightDummy;
while (head != null) {
if (head.val < x) {
leftCur.next = head;
leftCur = leftCur.next;
} else {
rightCur.next = head;
rightCur = rightCur.next;
}
head = head.next;
}
leftCur.next = rightDummy.next;
rightCur.next = null;
return leftDummy.next;
}
}
子集 II
问题描述
给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: [1,2,2]
输出:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
思路
回溯+排序去重
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<int> tmp;
vector<vector<int>> ans;
int len = nums.size();
if(len == 0)return ans;
dfs(nums,ans,tmp,len,0);
return ans;
}
void dfs(vector<int> &nums,vector<vector<int>> &ans,vector<int> tmp,int len,int k){
sort(tmp.begin(),tmp.end());
auto iter = find(ans.begin(),ans.end(),tmp);
if(iter == ans.end())ans.push_back(tmp);
for(int i = k;i<len;i++){
tmp.push_back(nums[i]);
dfs(nums,ans,tmp,len,i+1);
tmp.pop_back();
}
}
};
反转链表 II
题目描述
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
说明:
1 ≤ m ≤ n ≤ 链表长度。
示例:
输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL
解法
利用头插法,对 [m + 1, n] 范围内的元素逐一插入。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy;
int i = 0;
for (; i < m - 1; ++i) {
pre = pre.next;
}
ListNode head2 = pre;
pre = pre.next;
ListNode cur = pre.next;
for (; i < n - 1; ++i) {
pre.next = cur.next;
cur.next = head2.next;
head2.next = cur;
cur = pre.next;
}
return dummy.next;
}
}
二叉树的中序遍历
题目描述
给定一个二叉树,返回它的中序 遍历。
示例:
输入: [1,null,2,3]
1
\
2
/
3
输出: [1,3,2]
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
解法
- 递归算法
先递归左子树,再访问根结点,最后递归右子树。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
inorderTraversal(root, list);
return list;
}
private void inorderTraversal(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
inorderTraversal(root.left, list);
list.add(root.val);
inorderTraversal(root.right, list);
}
}
- 非递归算法
一直向左找到结点的最左结点,中间每到一个结点,将结点压入栈中。到了最左结点时,输出该结点值,然后对该结点右孩子执行上述循环。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
if (!stack.isEmpty()) {
root = stack.pop();
list.add(root.val);
root = root.right;
}
}
return list;
}
}
不同的二叉搜索树
题目描述
给定一个整数 n
,求以 1 ... n
为节点组成的二叉搜索树有多少种?
示例:
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
解法
原问题可拆解为子问题的求解。
二叉搜索树,可以分别以 1/2/3..n
做为根节点。所有情况累加起来,也就得到了最终结果。
res[n] 表示整数n组成的二叉搜索树个数。它的左子树可以有0/1/2...n-1
个节点,右子树可以有n-1/n-2...0
个节点。res[n] 是所有这些情况的加和。
时间复杂度分析:状态总共有 n
个,状态转移的复杂度是 O(n)
,所以总时间复杂度是 O(n²)
。
class Solution {
public int numTrees(int n) {
// res[n] 表示整数n组成的二叉搜索树个数
int[] res = new int[n + 1];
res[0] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 0; j < i; ++j) {
res[i] += res[j] * res[i - j - 1];
}
}
return res[n];
}
}
相同的树
题目描述
给定两个二叉树,编写一个函数来检验它们是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例 1:
输入: 1 1
/ \ / \
2 3 2 3
[1,2,3], [1,2,3]
输出: true
示例 2:
输入: 1 1
/ \
2 2
[1,2], [1,null,2]
输出: false
示例 3:
输入: 1 1
/ \ / \
2 1 1 2
[1,2,1], [1,1,2]
输出: false
二叉树的层次遍历
题目描述
给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。
例如:
给定二叉树: [3,9,20,null,null,15,7]
,
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
解法
利用队列,存储二叉树结点。size
表示每一层的结点数量,也就是内层循环的次数。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) {
return res;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> list = new ArrayList<>();
while ((size--) > 0) {
TreeNode node = queue.poll();
list.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
res.add(list);
}
return res;
}
}
从前序与中序遍历序列构造二叉树
问题描述
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
解法
利用树的前序遍历和中序遍历特性 + 递归实现。
对树进行前序遍历,每一个元素,在树的中序遍历中找到该元素;在中序遍历中,该元素的左边是它的左子树的全部元素,右边是它的右子树的全部元素,以此为递归条件,确定左右子树的范围。
/**
* Definition for a binary tree node.
* class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder == null || inorder == null || preorder.length != inorder.length) {
return null;
}
int n = preorder.length;
return n > 0 ? buildTree(preorder, 0, n - 1, inorder, 0, n - 1) : null;
}
private TreeNode buildTree(int[] preorder, int s1, int e1, int[] inorder, int s2, int e2) {
TreeNode node = new TreeNode(preorder[s1]);
if (s1 == e1 && s2 == e2) {
return node;
}
int p = s2;
while (inorder[p] != preorder[s1]) {
++p;
if (p > e2) {
throw new IllegalArgumentException("Invalid input!");
}
}
node.left = p > s2 ? buildTree(preorder, s1 + 1, s1 - s2 + p, inorder, s2, p - 1) : null;
node.right = p < e2 ? buildTree(preorder, s1 - s2 + p + 1, e1, inorder, p + 1, e2) : null;
return node;
}
}
从中序与后序遍历序列构造二叉树
问题描述
根据一棵树的中序遍历与后序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
解法
利用树的后序遍历和中序遍历特性 + 递归实现。
树的后序遍历序列,从后往前,对于每一个元素,在树的中序遍历中找到该元素;在中序遍历中,该元素的左边是它的左子树的全部元素,右边是它的右子树的全部元素,以此为递归条件,确定左右子树的范围。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] inorder, int[] postorder) {
if (inorder == null || postorder == null || inorder.length != postorder.length) {
return null;
}
int n = inorder.length;
return n > 0 ? buildTree(inorder, 0, n - 1, postorder, 0, n - 1) : null;
}
private TreeNode buildTree(int[] inorder, int s1, int e1, int[] postorder, int s2, int e2) {
TreeNode node = new TreeNode(postorder[e2]);
if (s2 == e2 && s1 == e1) {
return node;
}
int p = s1;
while (inorder[p] != postorder[e2]) {
++p;
if (p > e1) {
throw new IllegalArgumentException("Invalid input!");
}
}
node.left = p > s1 ? buildTree(inorder, s1, p - 1, postorder, s2, p - 1 + s2 - s1) : null;
node.right = p < e1 ? buildTree(inorder, p + 1, e1, postorder, p + s2 - s1, e2 - 1) : null;
return node;
}
}
路径总和
问题描述
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。
思路
题目要求有没有路径到叶子节点使和等于目标值
主要考察对叶子节点是否判断准确
这道题很简单,但是准确率不高,原因是的判断条件不明确,左空右不空返回什么什么,右空左不空返回什么什么,调试一直错
叶子节点唯一判断就是左右空
root->left == NULL && root->right==NULL
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool hasPathSum(TreeNode* root, int sum) {
if(root == NULL)return false;
if(root->right == NULL && root->left == NULL && sum == root->val)return true;
bool leftTrue = hasPathSum(root->left,sum - root->val);
bool rightTrue = hasPathSum(root->right,sum - root->val);
return (leftTrue || rightTrue);
}
};
杨辉三角
问题描述
给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
示例:
输入: 5
输出:
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]
思路
杨辉三角的特征是:
-
所有行的首元素和末尾元素都是1
-
除第一和第二行外,所有非首端末端元素都是前一行同列与前一列之和
matrix[i][j] = matrix[i-1][j]+matrix[i-1][j-1];
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> ans;
for(int i = 0;i<numRows;i++){
vector<int> tmp(i+1);
tmp[0] = 1;//最左侧为1
for(int j = 1;j<=i;j++){
if(i == j)//最右侧为1
{
tmp[j] = 1;
break;
}
tmp[j] = ans[i-1][j-1] + ans[i-1][j];
}
ans.push_back(tmp);
}
return ans;
}
};
const generate = function(numRows){
let arr = [];
for(let i = 0; i < numRows; i++){
let row = [];
row[0]=1;
row[i] = 1;
for(let j = 1; j < row.length - 1; j++){
row[j] = arr[i-1][j-1] + arr[i-1][j];
}
arr.push(row);
}
return arr;
}
杨辉三角 II
问题描述
给定一个非负索引 k,其中 k ≤ 33,返回杨辉三角的第 k 行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
示例:
输入: 3
输出: [1,3,3,1]
进阶:
你可以优化你的算法到 O(k) 空间复杂度吗?
思路
利用滚动数组的思想(和118题一样,省了行坐标):
因为每一个列坐标都是上一个行同列和前列之和,所以,只需要一组数组就可以模拟出来,并不需要二维数组
col[j] = col[j]+col[j-1];
注意:因为使用了j-1
列和j
列,为了防止重叠,不能从前往后跟新数组;只能从后往前
class Solution {
public:
vector<int> getRow(int rowIndex) {
vector<int> ans;
for(int i = 0;i <= rowIndex;i++){
for(int j = i-1;j > 0;j--){
ans[j] = ans[j-1] + ans[j];
}
ans.push_back(1);
}
return ans;
}
};
三角形最小路径和
问题描述
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
例如,给定三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
说明:
如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。
思想:
方法和119题如出一辙,都是利用滚动数组的原理(既对数组按特定规则进行条件层层处理,如同滚动)
方法是对每一个元素(除第一行)都构建最短路径
- 对于左右边界,最短路径只能是
triangle[i][0] = triangle[i-1][0]
以及
triangle[i][j] = triangle[i][j] + triangle[i-1][j-1] (j = i时成立)
- 对于非左右边界的其他任意值,其最短路径公式为
triangle[i][j] = triangle[i][j] + min(triangle[i-1][j],triangle[i-1][j-1]
-
对重新整理好的数组,遍历最下面一行元素,找最小值
-
O(1) 额外空间!!!!
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
size_t rowNum = triangle.size();
//特殊值处理
if(rowNum == 0)return 0;
if(rowNum == 1){
if(triangle[0].empty())return 0;
else return triangle[0][0];
}
for(int i = 1;i<rowNum;i++){
for(int j = i;j>=0;j--){
//边界处理
if(j == 0){triangle[i][j] = triangle[i][j] + triangle[i-1][j];continue;}
if(j == i){triangle[i][j] = triangle[i][j] + triangle[i-1][j-1];continue;}
//一般处理
triangle[i][j] = triangle[i][j] + min(triangle[i-1][j],triangle[i-1][j-1]);
}
}
int ans = INT_MAX;
for(auto v : triangle[rowNum-1]){
if(ans > v)ans = v;
}
return ans;
}
};
买卖股票的最佳时机
题目描述
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
解法
可以将整个数组中的数画到一张折现图中, 需要求的就是从图中找到一个波谷和一个波峰, 使其二者的差值最大化(其中波谷点需要在波峰点之前)。
我们可以维持两个变量, minprice(可能产生最大差值的波谷)初始值最大整数((1 << 31) -1), maxprofit(产生的最大差值)初始值 0, 然后迭代处理数组中的每个数进而优化两个变量的值; 如果数组元素prices[i]
不比minprice
大, 那就是目前为止最小波谷, 将 minprice=prices[i]
;如果数组元素prices[i]
比minprice
大, 那就判断prices[i]
与minprice
的差值是否比maxprofit
大, 如果是就更新maxprofit=prices[i]-minprice
, 并且minprice
即为波谷, prices[i]
为波谷; 否的话继续处理下一个数组元素。
买卖股票的最佳时机 II
题目描述
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
解法
问题描述中, 允许我们进行多次买入卖出操作, 同时还有一个隐含的条件---同一天内可以先卖出再买入;
我们只需要遵照涨买跌卖原则中的涨买原则, 不断比较连续两天中后一天是否比前一天股价高, 是的话就进行前一天买入, 后一天卖出, 累加利润。
买卖股票的最佳时机 III
问题描述
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这个情况下, 没有交易完成, 所以最大利润为 0。
思路:
建立两个数组,因为是只能两次交易,所以有左右数组
-
一个数组是
left[i]
,表示在第i天及之前交易的最大利润 -
一个数组是
right[i]
,表示在第i天及之后交易的最大利润
最后同时遍历,求和取出最大值就可以了
class Solution {
public:
int maxProfit(vector<int>& prices) {
int left = 0;
int len = prices.size();
if(len == 0 || len == 1)return 0;
vector<int> leftArr(len, 0);
vector<int> rightArr(len, 0);
int diff, day = 1, minPrice, maxPrice, maxProfit;
//计算某一天及之前的最大利益
minPrice = prices[0];
maxProfit = 0;
for (day = 1; day < len; day++) {
diff = prices[day] - minPrice;
if (diff < 0)minPrice = prices[day];
else if (diff > maxProfit)maxProfit = diff;
leftArr[day] = maxProfit;
}
//计算某一天及之前的最大利益
maxPrice = prices[len - 1];
maxProfit = 0;
for (day = len - 2; day >= 0; day--) {
diff = maxPrice - prices[day];
if (diff < 0)maxPrice = prices[day];
else if (diff > maxProfit)maxProfit = diff;
rightArr[day] = maxProfit;
}
int sum = 0;
maxProfit = leftArr[0] + rightArr[0];
for (int i = 1; i < len; i++) {
sum = leftArr[i] + rightArr[i];
if (sum > maxProfit)maxProfit = sum;
}
return maxProfit;
}
};
单词接龙
题目描述
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
-
每次转换只能改变一个字母。
-
转换过程中的中间单词必须是字典中的单词。
说明:
-
如果不存在这样的转换序列,返回 0。
-
所有单词具有相同的长度。
-
所有单词只由小写字母组成。
-
字典中不存在重复的单词。
-
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
输出: 5
解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",
返回它的长度 5。
示例 2:
输入:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]
输出: 0
解释: endWord "cog" 不在字典中,所以无法进行转换。
解法
利用广度优先搜索,level
表示层数,curNum
表示当前层的单词数,nextNum
表示下一层的单词数。
队首元素出队,对于该字符串,替换其中每一个字符为[a...z] 中的任一字符。判断新替换后的字符串是否在字典中。若是,若该字符串与 endWord 相等,直接返回 level + 1;若不等,该字符串入队,nextNum + 1,并将 该字符串从 wordSet 移除。
注意,每次只能替换一个字符,因此,在下一次替换前,需恢复为上一次替换前的状态。
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Queue<String> queue = new LinkedList<>();
// 需要转hashSet
Set<String> wordSet = new HashSet<>(wordList);
queue.offer(beginWord);
int level = 1;
int curNum = 1;
int nextNum = 0;
while (!queue.isEmpty()) {
String s = queue.poll();
--curNum;
char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; ++i) {
char ch = chars[i];
for (char j = 'a'; j <= 'z'; ++j) {
chars[i] = j;
String tmp = new String(chars);
// 字典中包含生成的中间字符串
if (wordSet.contains(tmp)) {
// 中间字符串与 endWord 相等
if (endWord.equals(tmp)) {
return level + 1;
}
// 中间字符串不是 endWord,则入队
queue.offer(tmp);
++nextNum;
// 确保之后不会再保存 tmp 字符串
wordSet.remove(tmp);
}
}
chars[i] = ch;
}
if (curNum == 0) {
curNum = nextNum;
nextNum = 0;
++level;
}
}
return 0;
}
}
被围绕的区域
题目描述
给定一个二维的矩阵,包含 'X'
和 'O'
(字母 O)。
找到所有被 'X'
围绕的区域,并将这些区域里所有的 'O'
用 'X'
填充。
示例:
X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:
X X X X
X X X X
X X X X
X O X X
解释:
被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O'
都不会被填充为 'X'
。 任何不在边界上,或不与边界上的 'O'
相连的 'O'
最终都会被填充为 'X'
。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
解法
逆向思维,从上下左右四个边缘的有效点'O'
往里搜索,将搜索到的有效点修改为'Y'
。最后,剩下的'O'
就是被'X'
包围的,将它们修改为'X'
,将'Y'
修改回'O'
。
class Solution {
/**
* 坐标点
*/
private class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public void solve(char[][] board) {
if (board == null || board.length < 3 || board[0].length < 3) {
return;
}
int m = board.length;
int n = board[0].length;
// top & bottom
for (int i = 0; i < n; ++i) {
bfs(board, 0, i);
bfs(board, m - 1, i);
}
// left & right
for (int i = 1; i < m - 1; ++i) {
bfs(board, i, 0);
bfs(board, i, n - 1);
}
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] == 'O') {
board[i][j] = 'X';
} else if (board[i][j] == 'Y') {
board[i][j] = 'O';
}
}
}
}
/**
* 广度优先搜索
* @param board
* @param i
* @param j
*/
private void bfs(char[][] board, int i, int j) {
Queue<Point> queue = new LinkedList<>();
if (isValid(board, i, j)) {
// 遇到'O',修改为'Y'
board[i][j] = 'Y';
queue.offer(new Point(i, j));
}
while (!queue.isEmpty()) {
Point p = queue.poll();
// 获取下一层所有有效坐标点,并加入队列
List<Point> points = getNextLevelValidPoints(board, p.x, p.y);
for (Point point : points) {
queue.offer(point);
}
}
}
/**
* 获取下一层所有有效坐标点,将这些坐标点修改为 'Y' 并返回
* @param board
* @param i
* @param j
* @return list
*/
private List<Point> getNextLevelValidPoints(char[][] board, int i, int j) {
List<Point> points = new ArrayList<>();
Point[] arr = new Point[] { new Point(i - 1, j), new Point(i + 1, j), new Point(i, j - 1),
new Point(i, j + 1) };
for (Point point : arr) {
if (isValid(board, point.x, point.y)) {
board[point.x][point.y] = 'Y';
points.add(point);
}
}
return points;
}
/**
* 判断坐标是否有效
* @param board
* @param i
* @param j
* @return boolean
*/
private boolean isValid(char[][] board, int i, int j) {
int m = board.length;
int n = board[0].length;
// 当前坐标对应的值是'O',才算有效
return i >= 0 && i <= m - 1 && j >= 0 && j <= n - 1 && board[i][j] == 'O';
}
}
只出现一次的数字
题目描述
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1]
输出: 1
示例 2:
输入: [4,1,2,1,2]
输出: 4
解法
任意数与 0 异或,都等于它本身。而任意数与自身异或,都等于 0。
class Solution {
public int singleNumber(int[] nums) {
int res = 0;
for (int num : nums) {
res ^= num;
}
return res;
}
}
只出现一次的数字 II
题目描述
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,3,2]
输出: 3
示例 2:
输入: [0,1,0,1,0,1,99]
输出: 99
解法
遍历数组元素,对于每一个元素,获得二进制位(0/1),累加到 bits 数组中,这样下来,出现三次的元素,bits 数组上的值一定能被 3 整除;找出不能被 3 整除的位,计算出实际的十进制数即可。
class Solution {
public int singleNumber(int[] nums) {
int[] bits = new int[32];
int n = nums.length;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < 32; ++j) {
bits[j] += ((nums[i] >> j) & 1);
}
}
int res = 0;
for (int i = 0; i < 32; ++i) {
if (bits[i] % 3 != 0) {
res += (1 << i);
}
}
return res;
}
}
复制带随机指针的链表
题目描述
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的深度拷贝。
解法
- 第一步,在每个节点的后面插入复制的节点;
- 第二步,对复制节点的 random 链接进行赋值;
- 第三步,分离两个链表。
/**
* Definition for singly-linked list with a random pointer.
* class RandomListNode {
* int label;
* RandomListNode next, random;
* RandomListNode(int x) { this.label = x; }
* };
*/
public class Solution {
public RandomListNode copyRandomList(RandomListNode head) {
if (head == null) {
return null;
}
// step1
RandomListNode cur = head;
while (cur != null) {
RandomListNode node = new RandomListNode(cur.label);
node.next = cur.next;
cur.next = node;
cur = node.next;
}
// step2
cur = head;
while (cur != null) {
RandomListNode clone = cur.next;
if (cur.random != null) {
clone.random = cur.random.next;
}
cur = clone.next;
}
// step3
cur = head;
RandomListNode cloneHead = head.next;
while (cur.next != null) {
RandomListNode clone = cur.next;
cur.next = clone.next;
cur = clone;
}
return cloneHead;
}
}
环形链表
题目描述
给定一个链表,判断链表中是否有环。
进阶:
你能否不使用额外空间解决此题?
解法
利用快慢指针,若快指针为 null,则不存在环,若快慢指针相遇,则存在环。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
return false;
}
}
环形链表 II
题目描述
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
说明: 不允许修改给定的链表。
进阶:
你是否可以不用额外空间解决此题?
解法
利用快慢指针,若快指针为 null
,则不存在环;若快慢指针相遇,则存在环,此时退出循环,利用 p1
指向链表头结点,p2
指向快慢指针相遇点,随后 p1
, p2
同时前进,相遇时(p1 == p2
)即为入环的第一个节点。
证明如下:
假设链表到入环点距离为 a
,入环点到快慢指针相遇点距离为 b
,慢指针行程为s
,快指针行程是它的 2 倍,即 2s
。相遇时快指针比慢指针多走了 n * r
圈。则:
① a + b = s
② 2s - s = n * r
-> a + b = n * r
-> a = n * r - b
= (n - 1) * r + r - b
r - b
为相遇点到入环点的距离。
p1
, p2
同时向前走 r - b
,p2
到达入环点,而 p1
距离入环点还有 (n - 1) * r
,双方同时走 (n - 1)
圈即可相遇,此时相遇点就是入环点!
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
boolean hasCycle = false;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
hasCycle = true;
break;
}
}
if (hasCycle) {
ListNode p1 = head;
ListNode p2 = slow;
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next;
}
return p1;
}
return null;
}
}
CPP
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if(head == NULL)return NULL;
ListNode *fast = head;
ListNode *slow = head;
while(fast != NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
if(fast == slow){
slow = head;
while(slow != fast){
fast = fast->next;
slow = slow->next;
}
return fast;
}
}
return NULL;//无环
}
};
重排链表
题目描述
给定一个单链表 L:L0→L1→…→Ln-1→Ln ,
将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→…
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
给定链表 1->2->3->4, 重新排列为 1->4->2->3.
示例 2:
给定链表 1->2->3->4->5, 重新排列为 1->5->2->4->3.
解法
先利用快慢指针找到链表的中间节点,之后对右半部分的链表进行reverse
。最后对两个链表进行合并。注意边界的判断。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public void reorderList(ListNode head) {
if (head == null || head.next == null || head.next.next == null) {
return;
}
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode slow = head;
ListNode fast = head;
ListNode pre = dummy;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
pre = pre.next;
}
pre.next = null;
// 将后半段的链表进行 reverse
ListNode rightPre = new ListNode(-1);
rightPre.next = null;
ListNode t = null;
while (slow != null) {
t = slow.next;
slow.next = rightPre.next;
rightPre.next = slow;
slow = t;
}
ListNode p1 = dummy.next;
ListNode p2 = p1.next;
ListNode p3 = rightPre.next;
ListNode p4 = p3.next;
while (p1 != null) {
p3.next = p1.next;
p1.next = p3;
if (p2 == null) {
break;
}
p1 = p2;
p2 = p1.next;
p3 = p4;
p4 = p3.next;
}
if (p4 != null) {
p1.next.next = p4;
}
head = dummy.next;
}
}
二叉树的前序遍历
题目描述
给定一个二叉树,返回它的 前序 遍历。
示例:
输入: [1,null,2,3]
1
\
2
/
3
输出: [1,2,3]
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
解法
- 递归算法
先访问根结点,再递归左子树,最后递归右子树。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
preorderTraversal(root, list);
return list;
}
private void preorderTraversal(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
list.add(root.val);
preorderTraversal(root.left, list);
preorderTraversal(root.right, list);
}
}
- 非递归算法
循环:先访问根结点,接着判断是否有右孩子,有则压入栈中;再判断是否有左孩子,有则压入栈中。判断栈是否为空,若是,跳出循环。否则弹出栈顶元素,继续循环。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
while (root != null) {
list.add(root.val);
if (root.right != null) {
stack.push(root.right);
}
if (root.left != null) {
stack.push(root.left);
}
if (!stack.isEmpty()) {
root = stack.pop();
} else {
break;
}
}
return list;
}
}
二叉树的后序遍历
题目描述
给定一个二叉树,返回它的 后序 遍历。
示例:
输入: [1,null,2,3]
1
\
2
/
3
输出: [3,2,1]
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
解法
- 递归算法
先递归左子树,再递归右子树,最后访问根结点。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
postorderTraversal(root, list);
return list;
}
private void postorderTraversal(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
postorderTraversal(root.left, list);
postorderTraversal(root.right, list);
list.add(root.val);
}
}
- 非递归算法
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
解法
用一个哈希表和一个双向链表来实现
-
哈希表保存每个节点的地址
-
链表头表示上次访问距离现在时间最短,尾部表示最近访问最少
-
访问节点,若节点存在,要把该节点放到链表头
-
插入时要考虑是否超过容量
//双向链表的节点
class Node{
public int key;
public int val;
public Node pre;//指向前面的指针
public Node next;//指向后面的指针
public Node(int key,int value){
this.val = value;
this.key = key;
}
}
class LRUCache {
int capacity;//容量
Node head;//双向链表的头,维护这个指针,因为set,get时需要在头部操作
Node end;//双向链表的尾,set时,要是满了,需要将链表的最后一个节点remove
HashMap<Integer,Node> map = new HashMap<Integer,Node>();//hash表
public LRUCache(int capacity) {
this.capacity = capacity;
}
//添加,删除尾部,插入头部的操作
public void remove(Node node){
Node cur = node;
Node pre = node.pre;
Node post = node.next;
if(pre == null){//说明cur是头部节点
head = post;
}
else pre.next = post;//更新指针,删除
if(post == null){//说明cur是最后的节点
end = pre;
}
else post.pre = pre;
}
public void setHead(Node node){
//直接插入
node.next = head;
node.pre = null;
if(head != null) head.pre = node;//防止第一次插入时为空
head = node;
if(end==null) end = node;
}
public int get(int key) {
if(map.containsKey(key)){
//需要把对应的节点调整到头部
Node latest = map.get(key);
remove(latest);
setHead(latest);
//返回value
return latest.val;
}
else return -1;
}
public void put(int key, int value) {
if(map.containsKey(key)){//这个key原来存在
//只需要把key对应的node提到最前面,更新value
Node oldNode = map.get(key);
oldNode.val = value;
remove(oldNode);
setHead(oldNode);
}
else{
//这个key原来不存在,需要重新new出来
Node newNode = new Node(key,value);
//接下来要考虑容量
if(map.size() < capacity){
setHead(newNode);
map.put(key, newNode);
}
else{
//容量不够,需要先将map中,最不常使用的那个删除了删除
map.remove(end.key);
//接下来更新双向链表
remove(end);
setHead(newNode);
//放入新的
map.put(key, newNode);
}
}
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
对链表进行插入排序
题目描述
对链表进行插入排序。
插入排序的动画演示如上。从第一个元素开始,该链表可以被认为已经部分排序(用黑色表示)。
每次迭代时,从输入数据中移除一个元素(用红色表示),并原地将其插入到已排好序的链表中。
插入排序算法:
-
插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
-
每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
-
重复直到所有输入数据插入完为止。
示例1:
输入: 4->2->1->3
输出: 1->2->3->4
示例2:
输入: -1->5->3->4->0
输出: -1->0->3->4->5
解法
从链表头部开始遍历,记录当前已完成插入排序的最后一个节点。然后进行以下操作:
-
获得要插入排序的节点 curNode 、其上一个节点 perNode 、其下一个节点 nextNode;
-
判断 curNode 是否应插入在 perNode 之后,若否,将 curNode 从链表中移除准备插入,若是,无需进一步操作,此时已排序的最后一个节点为 curNode;
-
在链表头节点前增加一个节点,应对 curNode 插入位置在 头节点之前的情况;
-
从头节点开始遍历,找到curNode 的插入位置,进行插入;
-
此时已排序的最后一个节点仍为 perNode ,重复以上操作直至链表末尾。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode insertionSortList(ListNode head) {
if (head == null) {
return null;
}
return insertionOneNode(head, head);
}
private ListNode insertionOneNode(ListNode head, ListNode node) {
if (head == null || node == null || node.next == null) {
return head;
}
ListNode perNode = node;
ListNode curNode = node.next;
ListNode nextNode = curNode.next;
if (node.val <= curNode.val) {
return insertionOneNode(head, curNode);
} else {
node.next = nextNode;
}
ListNode pNode = new ListNode(0);
pNode.next = head;
head = pNode;
while (pNode.next.val <= curNode.val) {
pNode = pNode.next;
}
ListNode nNode = pNode.next;
pNode.next = curNode;
curNode.next = nNode;
return insertionOneNode(head.next, perNode);
}
}
逆波兰表达式求值
题目描述
根据逆波兰表示法,求表达式的值。
有效的运算符包括 +
, -
, *
, /
。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
说明:
整数除法只保留整数部分。
给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
示例 1:
输入: ["2", "1", "+", "3", "*"]
输出: 9
解释: ((2 + 1) * 3) = 9
示例 2:
输入: ["4", "13", "5", "/", "+"]
输出: 6
解释: (4 + (13 / 5)) = 6
示例 3:
输入: ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]
输出: 22
解释:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
解法
遍历数组,若遇到操作数,将其压入栈中;若遇到操作符,从栈中弹出右操作数和左操作数,将运算结果压入栈中。最后栈中唯一的元素就是结果。
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for (String e : tokens) {
if (isNum(e)) {
stack.push(Integer.parseInt(e));
} else {
int y = stack.pop();
int x = stack.pop();
int z = 0;
switch (e) {
case "+": z = x + y; break;
case "-": z = x - y; break;
case "*": z = x * y; break;
case "/": z = x / y; break;
}
stack.push(z);
}
}
return stack.peek();
}
private boolean isNum(String val) {
try {
Integer.parseInt(val);
return true;
} catch (Exception e) {
return false;
}
}
}
寻找旋转排序数组中的最小值
题目描述
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
示例 1:
输入: [3,4,5,1,2]
输出: 1
示例 2:
输入: [4,5,6,7,0,1,2]
输出: 0
解法
利用两个指针 p,q 指示数组的首尾两端,若 nums[p] <= nums[q],说明数组呈递增趋势,返回 nums[p];否则判断 nums[p] 与 nums[mid] 的大小,从而决定新的数组首尾两端,循环二分查找。
class Solution {
public int findMin(int[] nums) {
int n = nums.length;
if (n == 1) {
return nums[0];
}
int p = 0;
int q = n - 1;
int mid = p + ((q - p) >> 1);
while (p < q) {
if (nums[p] <= nums[q]) {
break;
}
if (nums[p] > nums[mid]) {
q = mid;
} else {
p = mid + 1;
}
mid = p + ((q - p) >> 1);
}
return nums[p];
}
}
最小栈
题目描述
设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
-
push(x) -- 将元素 x 推入栈中。
-
pop() -- 删除栈顶的元素。
-
top() -- 获取栈顶元素。
-
getMin() -- 检索栈中的最小元素。
示例:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
解法
创建两个栈 stack
, help
,help
作为辅助栈,每次压栈时,若 help
栈顶元素大于/等于要压入的元素 x
,或者 help
栈为空,则压入 x
,否则重复压入栈顶元素。取最小元素时,从 help
中获取栈顶元素即可。
class MinStack {
private Stack<Integer> stack;
private Stack<Integer> help;
/** initialize your data structure here. */
public MinStack() {
stack = new Stack<>();
help = new Stack<>();
}
public void push(int x) {
stack.push(x);
help.push(help.isEmpty() || help.peek() >= x ? x : help.peek());
}
public void pop() {
stack.pop();
help.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return help.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(x);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
然后还可以再优化一点点, 就是在辅助栈存最小值的索引, 这样可以避免辅助栈的大量的重复元素的情况.
/**
* initialize your data structure here.
*/
const MinStack = function() {
this.arr = [];
this.help = [];
};
/**
* @param {number} x
* @return {void}
*/
MinStack.prototype.push = function(x) {
this.arr.push(x);
if(this.help.length === 0){
this.help.push(0);
}else{
let min = this.getMin();
if(x < min){
this.help.push(this.arr.length-1);
}
}
};
/**
* @return {void}
*/
MinStack.prototype.pop = function() {
if(this.arr.length === 0){
throw new Error('???');
}
if(this.arr.length - 1 === this.help[this.help.length - 1]){
this.help.pop();
}
this.arr.pop();
};
/**
* @return {number}
*/
MinStack.prototype.top = function() {
return this.arr[this.arr.length-1];
};
/**
* @return {number}
*/
MinStack.prototype.getMin = function() {
if(this.arr.length === 0){
throw new Error("???");
}
return this.arr[this.help[this.help.length-1]];
};
/**
* Your MinStack object will be instantiated and called as such:
* var obj = Object.create(MinStack).createNew()
* obj.push(x)
* obj.pop()
* var param_3 = obj.top()
* var param_4 = obj.getMin()
*/
相交链表
题目描述
编写一个程序,找到两个单链表相交的起始节点。
例如,下面的两个链表:
A: a1 → a2
↘
c1 → c2 → c3
↗
B: b1 → b2 → b3
在节点 c1 开始相交。
注意:
-
如果两个链表没有交点,返回
null
. -
在返回结果后,两个链表仍须保持原有的结构。
-
可假定整个链表结构中没有循环。
-
程序尽量满足
O(n)
时间复杂度,且仅用O(1)
内存。
解法
-
第一遍循环,找出两个链表的长度差 N;
-
第二遍循环,长链表先走 N 步,然后同时移动,判断是否有相同节点。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
int lenA = 0, lenB = 0;
ListNode p = headA, q = headB;
while (p != null) {
p = p.next;
++lenA;
}
while (q != null) {
q = q.next;
++lenB;
}
p = headA;
q = headB;
if (lenA > lenB) {
for (int i = 0; i < lenA - lenB; ++i) {
p = p.next;
}
} else {
for (int i = 0; i < lenB - lenA; ++i) {
q = q.next;
}
}
while (p != null && p != q) {
p = p.next;
q = q.next;
}
return p;
}
}
Excel表列名称
题目描述
给定一个正整数,返回它在 Excel 表中相对应的列名称。
例如,
1 -> A
2 -> B
3 -> C
...
26 -> Z
27 -> AA
28 -> AB
...
示例 1:
输入: 1
输出: "A"
示例 2:
输入: 28
输出: "AB"
示例 3:
输入: 701
输出: "ZY"
解法
-
将数字 n 减一,则尾数转换为 0-25 的 26 进制数的个位;用除 26 取余的方式,获得尾数对应的符号。
-
用除26取余的方式,获得尾数对应的符号;
-
重复步骤1、2直至 n 为 0。
class Solution {
public String convertToTitle(int n) {
if (n < 0) {
return "";
}
StringBuilder sb = new StringBuilder();
while (n > 0) {
n--;
int temp = n % 26;
sb.insert(0,(char)(temp + 'A'));
n /= 26;
}
return sb.toString();
}
}
组合两个表
题目描述
表1: Person
+-------------+---------+
| 列名 | 类型 |
+-------------+---------+
| PersonId | int |
| FirstName | varchar |
| LastName | varchar |
+-------------+---------+
PersonId 是上表主键
表2: Address
+-------------+---------+
| 列名 | 类型 |
+-------------+---------+
| AddressId | int |
| PersonId | int |
| City | varchar |
| State | varchar |
+-------------+---------+
AddressId 是上表主键
编写一个 SQL 查询,满足条件:无论 person 是否有地址信息,都需要基于上述两表提供 person 的以下信息:
FirstName, LastName, City, State
解法
题意中说无论 person
是否有地址信息,都要查出来,因此,使用左外连接查询。注意使用 on
关键字。
# Write your MySQL query statement below
select a.FirstName, a.LastName, b.City, b.State from Person a left join Address b on a.PersonId = b.PersonId;
Input
{
"headers": {
"Person": [
"PersonId",
"LastName",
"FirstName"
],
"Address": [
"AddressId",
"PersonId",
"City",
"State"
]
},
"rows": {
"Person": [
[
1,
"Wang",
"Allen"
]
],
"Address": [
[
1,
2,
"New York City",
"New York"
]
]
}
}
Output
{
"headers": [
"FirstName",
"LastName",
"City",
"State"
],
"values": [
[
"Allen",
"Wang",
null,
null
]
]
}
第二高的薪水
题目描述
编写一个 SQL 查询,获取 Employee
表中第二高的薪水(Salary) 。
+----+--------+
| Id | Salary |
+----+--------+
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
+----+--------+
例如上述 Employee
表,SQL查询应该返回 200
作为第二高的薪水。如果不存在第二高的薪水,那么查询应返回 null
。
+---------------------+
| SecondHighestSalary |
+---------------------+
| 200 |
+---------------------+
解法
从小于最高薪水的记录里面选最高即可。
# Write your MySQL query statement below
select max(Salary) SecondHighestSalary from Employee where Salary < (Select max(Salary) from Employee)
Input
{
"headers": {
"Employee": [
"Id",
"Salary"
]
},
"rows": {
"Employee": [
[
1,
100
],
[
2,
200
],
[
3,
300
]
]
}
}
Output
{
"headers": [
"SecondHighestSalary"
],
"values": [
[
200
]
]
}
第N高的薪水
题目描述
编写一个 SQL 查询,获取 Employee
表中第 n 高的薪水(Salary)。
+----+--------+
| Id | Salary |
+----+--------+
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
+----+--------+
例如上述 Employee
表,n = 2 时,应返回第二高的薪水 200
。如果不存在第 n 高的薪水,那么查询应返回 null。
+------------------------+
| getNthHighestSalary(2) |
+------------------------+
| 200 |
+------------------------+
解法
对 Salary 进行分组,然后根据 Salary 降序排列。选出偏移为 n-1 的一个记录即可。
CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT
BEGIN
SET N = N - 1;
RETURN (
# Write your MySQL query statement below.
select Salary from Employee group by Salary order by Salary desc limit 1 offset N
);
END
Input
{
"headers": {
"Employee": [
"Id",
"Salary"
]
},
"argument": 2,
"rows": {
"Employee": [
[
1,
100
],
[
2,
200
],
[
3,
300
]
]
}
}
Output
{
"headers": [
"getNthHighestSalary(2)"
],
"values": [
[
200
]
]
}
分数排名
题目描述
编写一个 SQL 查询来实现分数排名。如果两个分数相同,则两个分数排名(Rank)相同。请注意,平分后的下一个名次应该是下一个连续的整数值。换句话说,名次之间不应该有“间隔”。
+----+-------+
| Id | Score |
+----+-------+
| 1 | 3.50 |
| 2 | 3.65 |
| 3 | 4.00 |
| 4 | 3.85 |
| 5 | 4.00 |
| 6 | 3.65 |
+----+-------+
例如,根据上述给定的 Scores
表,你的查询应该返回(按分数从高到低排列):
+-------+------+
| Score | Rank |
+-------+------+
| 4.00 | 1 |
| 4.00 | 1 |
| 3.85 | 2 |
| 3.65 | 3 |
| 3.65 | 3 |
| 3.50 | 4 |
+-------+------+
解法
对于每一个分数,找出表中有多少个 >=
该分数的不同的(distinct)分数,然后按降序排列。
# Write your MySQL query statement below
select Score, (select count(distinct Score) from Scores where Score >= s.Score) Rank from Scores s order by Score desc;
Input
{
"headers": {
"Scores": [
"Id",
"Score"
]
},
"rows": {
"Scores": [
[
1,
3.50
],
[
2,
3.65
],
[
3,
4.00
],
[
4,
3.85
],
[
5,
4.00
],
[
6,
3.65
]
]
}
}
Output
{
"headers": [
"Score",
"Rank"
],
"values": [
[
4.0,
1
],
[
4.0,
1
],
[
3.85,
2
],
[
3.65,
3
],
[
3.65,
3
],
[
3.5,
4
]
]
}
连续出现的数字
题目描述
编写一个 SQL 查询,查找所有至少连续出现三次的数字。
+----+-----+
| Id | Num |
+----+-----+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | 1 |
| 6 | 2 |
| 7 | 2 |
+----+-----+
例如,给定上面的 Logs
表, 1
是唯一连续出现至少三次的数字。
+-----------------+
| ConsecutiveNums |
+-----------------+
| 1 |
+-----------------+
解法
联表查询。
# Write your MySQL query statement below
select distinct l1.Num ConsecutiveNums
from Logs l1
join Logs l2 on l1.Id = l2.Id - 1
join Logs l3 on l2.Id = l3.Id - 1
where l1.Num = l2.Num and l2.Num = l3.Num
Input
{"headers": {"Logs": ["Id", "Num"]}, "rows": {"Logs": [[1, 1], [2, 1], [3, 1], [4, 2], [5, 1], [6, 2], [7, 2]]}}
Output
{"headers":["ConsecutiveNums"],"values":[[1]]}
连续出现的数字
题目描述
Employee
表包含所有员工,他们的经理也属于员工。每个员工都有一个 Id,此外还有一列对应员工的经理的 Id。
+----+-------+--------+-----------+
| Id | Name | Salary | ManagerId |
+----+-------+--------+-----------+
| 1 | Joe | 70000 | 3 |
| 2 | Henry | 80000 | 4 |
| 3 | Sam | 60000 | NULL |
| 4 | Max | 90000 | NULL |
+----+-------+--------+-----------+
给定 Employee
表,编写一个 SQL 查询,该查询可以获取收入超过他们经理的员工的姓名。在上面的表格中,Joe 是唯一一个收入超过他的经理的员工。
+----------+
| Employee |
+----------+
| Joe |
+----------+
解法
联表查询。
# Write your MySQL query statement below
# select s.Name as Employee
# from Employee s
# where s.ManagerId is not null and s.Salary > (select Salary from Employee where Id = s.ManagerId);
select s1.Name as Employee
from Employee s1
inner join Employee s2
on s1.ManagerId = s2.Id
where s1.Salary > s2.Salary;
Input
{"headers": {"Employee": ["Id", "Name", "Salary", "ManagerId"]}, "rows": {"Employee": [[1, "Joe", 70000, 3], [2, "Henry", 80000, 4], [3, "Sam", 60000, null], [4, "Max", 90000, null]]}}
Output
{"headers":["Employee"],"values":[["Joe"]]}
查找重复的电子邮箱
题目描述
编写一个 SQL 查询,查找 Person
表中所有重复的电子邮箱。
示例:
+----+---------+
| Id | Email |
+----+---------+
| 1 | a@b.com |
| 2 | c@d.com |
| 3 | a@b.com |
+----+---------+
根据以上输入,你的查询应返回以下结果:
+---------+
| Email |
+---------+
| a@b.com |
+---------+
说明:所有电子邮箱都是小写字母。
解法
对 Email
进行分组,选出数量大于 1 的 Email
即可。
# Write your MySQL query statement below
select Email
from Person
group by Email
having count(Email) > 1;
Input
{"headers": {"Person": ["Id", "Email"]}, "rows": {"Person": [[1, "a@b.com"], [2, "c@d.com"], [3, "a@b.com"]]}}
Output
{"headers":["Email"],"values":[["a@b.com"]]}
从不订购的客户
题目描述
某网站包含两个表,Customers
表和 Orders
表。编写一个 SQL 查询,找出所有从不订购任何东西的客户。
Customers
表:
+----+-------+
| Id | Name |
+----+-------+
| 1 | Joe |
| 2 | Henry |
| 3 | Sam |
| 4 | Max |
+----+-------+
Orders
表:
+----+------------+
| Id | CustomerId |
+----+------------+
| 1 | 3 |
| 2 | 1 |
+----+------------+
例如给定上述表格,你的查询应返回:
+-----------+
| Customers |
+-----------+
| Henry |
| Max |
+-----------+
解法
两个表左连接,找出 CustomerId
为 null
的记录即可。
# Write your MySQL query statement below
select Name as Customers
from Customers c
left join Orders o
on c.Id = o.CustomerId
where o.CustomerId is null
Input
{"headers": {"Customers": ["Id", "Name"], "Orders": ["Id", "CustomerId"]}, "rows": {"Customers": [[1, "Joe"], [2, "Henry"], [3, "Sam"], [4, "Max"]], "Orders": [[1, 3], [2, 1]]}}
Output
{"headers":["Customers"],"values":[["Henry"],["Max"]]}
部门工资最高的员工
题目描述
Employee
表包含所有员工信息,每个员工有其对应的 Id, salary 和 department Id。
+----+-------+--------+--------------+
| Id | Name | Salary | DepartmentId |
+----+-------+--------+--------------+
| 1 | Joe | 70000 | 1 |
| 2 | Henry | 80000 | 2 |
| 3 | Sam | 60000 | 2 |
| 4 | Max | 90000 | 1 |
+----+-------+--------+--------------+
Department
表包含公司所有部门的信息。
+----+----------+
| Id | Name |
+----+----------+
| 1 | IT |
| 2 | Sales |
+----+----------+
编写一个 SQL 查询,找出每个部门工资最高的员工。例如,根据上述给定的表格,Max 在 IT 部门有最高工资,Henry 在 Sales 部门有最高工资。
+------------+----------+--------+
| Department | Employee | Salary |
+------------+----------+--------+
| IT | Max | 90000 |
| Sales | Henry | 80000 |
+------------+----------+--------+
解法
先对 Employee
表根据 DepartmentId
进行分组,找出每组中工资最高的员工,再与 Department
表进行内连接,即可查出结果。
# Write your MySQL query statement below
select d.Name as Department, c.Name as Employee, c.Salary
from (select a.Name, a.DepartmentId, a.Salary from Employee a
inner join (select DepartmentId, max(Salary) Salary from Employee group by DepartmentId) b
on a.DepartMentId = b.DepartMentId and a.Salary = b.Salary) c
inner join DepartMent d
on d.Id = c.DepartmentId;
Input
{"headers": {"Employee": ["Id", "Name", "Salary", "DepartmentId"], "Department": ["Id", "Name"]}, "rows": {"Employee": [[1, "Joe", 70000, 1], [2, "Henry", 80000, 2], [3, "Sam", 60000, 2], [4, "Max", 90000, 1]], "Department": [[1, "IT"], [2, "Sales"]]}}
Output
{"headers":["Department","Employee","Salary"],"values":[["Sales","Henry",80000],["IT","Max",90000]]}
买卖股票的最佳时机 IV
问题描述
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [2,4,1], k = 2
输出: 2
解释: 在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
示例 2:
输入: [3,2,6,5,0,3], k = 2
输出: 7
解释: 在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
思路:
以解决DP问题的方式处理此问题, 涉及股票天数, 交易次数, 交易内容(买入OR卖出)三个元素, 枚举这三种元素叠加产生的所有情况, 不断
在每一个选择之后保证最优解, 最后我们看最后一天股票, 经历最大交易次数后, 并且状态是卖出时所得的收益即为最大收益;
旋转数组
题目描述
给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
示例 1:
输入: [1,2,3,4,5,6,7] 和 k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入: [-1,-100,3,99] 和 k = 2
输出: [3,99,-1,-100]
解释:
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]
说明:
-
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
-
要求使用空间复杂度为 O(1) 的原地算法。
解法
先对整个数组做翻转,之后分别对 [0, k - 1]、[k, n - 1] 的子数组进行翻转,即可得到结果。
class Solution {
public void rotate(int[] nums, int k) {
int n = nums.length;
k %= n;
if (n < 2 || k == 0) {
return;
}
rotate(nums, 0, n - 1);
rotate(nums, 0, k - 1);
rotate(nums, k, n - 1);
}
private void rotate(int[] nums, int start, int end) {
while (start < end) {
int t = nums[start];
nums[start] = nums[end];
nums[end] = t;
++start;
--end;
}
}
}
删除重复的电子邮箱
题目描述
编写一个 SQL 查询,来删除 Person
表中所有重复的电子邮箱,重复的邮箱里只保留 Id 最小 的那个。
+----+------------------+
| Id | Email |
+----+------------------+
| 1 | john@example.com |
| 2 | bob@example.com |
| 3 | john@example.com |
+----+------------------+
Id 是这个表的主键。
例如,在运行你的查询语句之后,上面的 Person
表应返回以下几行:
+----+------------------+
| Id | Email |
+----+------------------+
| 1 | john@example.com |
| 2 | bob@example.com |
+----+------------------+
解法
先根据 Email
分组,选出每个组中最小的 Id
,作为一张临时表,再删除掉所有 Id 不在这张临时表的记录。
# Write your MySQL query statement below
# 用这里注释的语句运行会报错,不能 select 之后再 update
# You can't specify target table 'Person' for update in FROM clause
# delete from Person
# where Id not in(
# select min(Id) as Id
# from Person
# group by Email);
delete from Person
where Id not in(
select Id from (
select min(id) as Id
from Person
group by Email
) a
);
Input
{"headers": {"Person": ["Id", "Email"]}, "rows": {"Person": [[1, "john@example.com"], [2, "bob@example.com"], [3, "john@example.com"]]}}
Output
{"headers":["Id","Email"],"values":[[1,"john@example.com"],[2,"bob@example.com"]]}
上升的温度
题目描述
给定一个 Weather
表,编写一个 SQL 查询,来查找与之前(昨天的)日期相比温度更高的所有日期的 Id。
+---------+------------------+------------------+
| Id(INT) | RecordDate(DATE) | Temperature(INT) |
+---------+------------------+------------------+
| 1 | 2015-01-01 | 10 |
| 2 | 2015-01-02 | 25 |
| 3 | 2015-01-03 | 20 |
| 4 | 2015-01-04 | 30 |
+---------+------------------+------------------+
例如,根据上述给定的 Weather
表格,返回如下 Id:
+----+
| Id |
+----+
| 2 |
| 4 |
+----+
解法
利用 datediff 函数返回两个日期相差的天数。
# Write your MySQL query statement below
select a.Id
from Weather a
inner join Weather b
on a.Temperature > b.Temperature and datediff(a.RecordDate, b.RecordDate) = 1;
Input
{"headers": {"Weather": ["Id", "RecordDate", "Temperature"]}, "rows": {"Weather": [[1, "2015-01-01", 10], [2, "2015-01-02", 25], [3, "2015-01-03", 20], [4, "2015-01-04", 30]]}}
Output
{"headers":["Id"],"values":[[2],[4]]}
打家劫舍
题目描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。偷窃到的最高金额 = 2 + 9 + 1 = 12 。
解法
每个房屋都有两种选择,抢 or 不抢。从第 n 个房屋开始往前抢,进行递归。
以上递归方式会超时,因为中间有很多重复的计算,我们可以开一个空间,如数组,来记录中间结果,避免重复计算,这也就是动态规划。
递归版(超时):
class Solution {
private int process(int n, int[] nums) {
return n < 0 ? 0 : Math.max(nums[n] + process(n - 2, nums), process(n - 1, nums));
}
public int rob(int[] nums) {
return (nums == null || nums.length == 0) ? 0 : process(nums.length - 1, nums);
}
}
动态规划版(改进):
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
if (n == 1) {
return nums[0];
}
int[] result = new int[n];
result[0] = nums[0];
result[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < n; ++i) {
result[i] = Math.max(nums[i] + result[i - 2], result[i - 1]);
}
return result[n - 1];
}
}
岛屿的个数
题目描述
给定一个由'1'
(陆地)和'0'
(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
解法
对网格进行遍历,每遇到一个岛屿,都将其中陆地1
全部变成0
,则遇到岛屿的总数即为所求。
class Solution:
def numIslands(self, grid):
def dp(x, y):
if x >= 0 and x < len(grid) and y >= 0 and y < len(grid[x]) and grid[x][y] == '1':
grid[x][y] = '0'
dp(x-1, y)
dp(x+1, y)
dp(x, y-1)
dp(x, y+1)
ans = 0
for i in range(len(grid)):
for j in range(len(grid[i])):
if grid[i][j] == '1':
ans += 1
dp(i, j)
return ans
快乐数
题目描述
编写一个算法来判断一个数是不是“快乐数”。
一个“快乐数”定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1。如果可以变为 1,那么这个数就是快乐数。
示例:
输入: 19
输出: true
解释:
1² + 9² = 82
8² + 2² = 68
6² + 8² = 100
1² + 0² + 0² = 1
解法
在进行验算的过程中,发现一个规律,只要过程中得到任意一个结果和为 4,那么就一定会按 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4
进行循环,这样的数就不为快乐数;此外,结果和与若是与输入 n 或者上一轮结果和 n 相同,那也不为快乐数。
class Solution {
public boolean isHappy(int n) {
if (n <= 0) return false;
int sum = 0;
while (sum != n) {
while (n > 0) {
sum += Math.pow(n % 10, 2);
n /= 10;
}
if (sum == 1) return true;
if (sum == 4) return false;
n = sum;
sum = 0;
}
return false;
}
}
删除链表中的结点
题目描述
删除链表中等于给定值 val 的所有节点。
示例:
输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5
解法
利用链表天然的递归性。首先判断头结点 head
的值是否为 val,若是,那么最终链表是 head->removeElements(head.next, val);若不是,则为 removeElements(head.next, val);
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return null;
}
head.next = removeElements(head.next, val);
return head.val != val ? head : head.next;
}
}
课程表
题目描述
现在你总共有 n
门课需要选,记为 0
到 n-1
。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
。
给定课程总量以及它们的先决条件,判断是否可能完成所有课程的学习?
示例1:
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例2:
输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
说明:
-
输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
-
你可以假定输入的先决条件中没有重复的边。
提示:
-
这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
-
拓扑排序也可以通过 BFS 完成。
解法
利用拓扑排序,拓扑排序是说,如果存在 a -> b
的边,那么拓扑排序的结果,a 一定在 b 的前面。
如果存在拓扑排序,那么就可以完成所有课程的学习。
说明:
-
拓扑排序的本质是不断输出入度为 0 的点,该算法可用于判断图中是否存在环;
-
可以用队列(或者栈)保存入度为 0 的点,避免每次遍历所有的点。
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] indegree = new int[numCourses];
int[][] g = new int[numCourses][numCourses];
for (int[] e : prerequisites) {
int cur = e[0];
int pre = e[1];
if (g[pre][cur] == 0) {
++indegree[cur];
g[pre][cur] = 1;
}
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; ++i) {
if (indegree[i] == 0) {
queue.offer(i);
}
}
int cnt = 0;
while (!queue.isEmpty()) {
int i = queue.poll();
++cnt;
for (int j = 0; j < numCourses; ++j) {
if (g[i][j] == 1) {
g[i][j] = 0;
--indegree[j];
if (indegree[j] == 0) {
queue.offer(j);
}
}
}
}
return cnt == numCourses;
}
}
数组中的第K个最大元素
题目描述
在未排序的数组中找到第 k
个最大的元素。请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
示例1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
解法
建一个小根堆,遍历数组元素:
-
当堆中元素个数少于堆容量时,直接添加该数组元素;
-
否则,当堆顶元素小于数组元素时,先弹出堆顶元素,再把数组元素添加进堆。
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>(k);
for (int e : nums) {
if (minHeap.size() < k) {
minHeap.add(e);
} else {
if (minHeap.peek() < e) {
minHeap.poll();
minHeap.add(e);
}
}
}
return minHeap.peek();
}
}
汇总区间
问题描述
给定一个无重复元素的有序整数数组,返回数组区间范围的汇总。
示例 1:
输入: [0,1,2,4,5,7]
输出: ["0->2","4->5","7"]
解释: 0,1,2 可组成一个连续的区间; 4,5 可组成一个连续的区间。
示例 2:
输入: [0,2,3,4,6,8,9]
输出: ["0","2->4","6","8->9"]
解释: 2,3,4 可组成一个连续的区间; 8,9 可组成一个连续的区间。
思路
-
设置count计数有多少个连续数字
-
当不连续时,得出一个区间加入答案,更改下标
idx += (count+1)
,且count重新置0
class Solution {
public:
vector<string> summaryRanges(vector<int>& nums) {
int len = nums.size();
if(len == 0)return {};
vector<string> ans;
int count = 0;
int idx = 0;
while((idx + count) < len-1){
if(nums[idx+count] == nums[idx+count+1]-1)count++;
else{
string str;
if(count == 0){
str = to_string(nums[idx]);
}
else{
str = to_string(nums[idx])+"->"+to_string(nums[idx+count]);
}
ans.push_back(str);
idx += (count+1);
count = 0;
}
}
//末尾处理
string str;
if(count > 0)
str = to_string(nums[idx])+"->"+to_string(nums[idx+count]);
else
str = to_string(nums[idx]);
ans.push_back(str);
return ans;
}
};
2的幂
题目描述
给定一个整数,编写一个函数来判断它是否是 2 的幂次方。
示例 1:
输入: 1
输出: true
解释: 2^0 = 1
示例 2:
输入: 16
输出: true
解释: 2^4 = 16
示例 3:
输入: 218
输出: false
解法
解法一
可以利用 2^31 对该数取余,结果为 0 则为 2 的幂次方。
class Solution {
public boolean isPowerOfTwo(int n) {
return n > 0 && 1073741824 % n == 0;
}
}
解法二
也可以循环取余,每次除以 2,判断最终结果是否为 1。
class Solution {
public boolean isPowerOfTwo(int n) {
if (n < 1) {
return false;
}
while (n % 2 == 0) {
n >>= 1;
}
return n == 1;
}
}
解法三
利用 n & -n
:
n & -n 表示 n 的二进制表示的最右边一个1
只要 (n & -n) == n,说明 n 的二进制表示中只有一个 1,那么也就说明它是 2 的幂。
class Solution {
public boolean isPowerOfTwo(int n) {
return n > 0 && (n & -n) == n;
}
}
回文链表
题目描述
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
解法
利用快慢指针,找到链表的中间节点,之后对右部分链表进行逆置,然后左右两部分链表依次遍历,若出现对应元素不相等的情况,返回 false
;否则返回 true
。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
if (fast != null) {
slow = slow.next;
}
ListNode rightPre = new ListNode(-1);
while (slow != null) {
ListNode t = slow.next;
slow.next = rightPre.next;
rightPre.next = slow;
slow = t;
}
ListNode p1 = rightPre.next;
ListNode p2 = head;
while (p1 != null) {
if (p1.val != p2.val) {
return false;
}
p1 = p1.next;
p2 = p2.next;
}
return true;
}
}
二叉搜索树的最近公共祖先
题目描述
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
_______6______
/ \
___2__ ___8__
/ \ / \
0 _4 7 9
/ \
3 5
示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
说明:
-
所有节点的值都是唯一的。
-
p、q 为不同节点且均存在于给定的二叉搜索树中。
解法
如果 root 左子树存在 p,右子树存在 q,那么 root 为最近祖先;
如果 p、q 均在 root 左子树,递归左子树;右子树同理。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root;
}
TreeNode leftNode = lowestCommonAncestor(root.left, p, q);
TreeNode rightNode = lowestCommonAncestor(root.right, p, q);
if (leftNode != null && rightNode != null) {
return root;
}
return leftNode != null ? leftNode : (rightNode != null) ? rightNode : null;
}
}
删除链表中的节点
题目描述
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。
现有一个链表 -- head = [4,5,1,9],它可以表示为:
4 -> 5 -> 1 -> 9
示例 1:
输入: head = [4,5,1,9], node = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
示例 2:
输入: head = [4,5,1,9], node = 1
输出: [4,5,9]
解释: 给定你链表中值为 1 的第三个节点,那么在调用了你的函数之后,该链表应变为 4 -> 5 -> 9.
说明:
-
链表至少包含两个节点。
-
链表中所有节点的值都是唯一的。
-
给定的节点为非末尾节点并且一定是链表中的一个有效节点。
-
不要从你的函数中返回任何结果。
解法
刚开始看到这道题,有点懵,明明题目给出的输入是 head 跟 node,为什么 solution 中只有 node,后来才明白,只提供 node 依然可以解决此题。只要把下个结点的 值 & next 赋给当前 node,然后删除下个结点,就可以搞定。好题!
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public void deleteNode(ListNode node) {
// 保存下一个结点
ListNode tmp = node.next;
// 将下个结点的值赋给当前要删除的结点
node.val = node.next.val;
node.next = node.next.next;
// tmp 置为空,让 jvm 进行垃圾回收
tmp = null;
}
}
移动0
问题描述
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
-
必须在原数组上操作,不能拷贝额外的数组。
-
尽量减少操作次数。
思路
两种思路,分别是
-
快慢指针,慢指针找0,快指针找慢指针之后的非0元素和慢指针交换,没有找到就直接结束
-
也可以通过对非0元素遍历来实现(更好)
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int len = nums.size();
if(len == 0)return;
int slow = 0;
int fast;
while(slow < len){
if(nums[slow] == 0){
fast = slow+1;
while(fast < len){
if(nums[fast] == 0)fast++;
else break;
}
if(fast == len)return;
swap(nums[slow],nums[fast]);
}
slow++;
}
}
};
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int len = nums.size();
if(len == 0)return;
int idx = 0;
for(int i = 0;i<len;i++){
if(nums[i] != 0){
nums[idx] = nums[i];
idx++;
}
}
for(int i = idx;i<len;i++){
nums[i] = 0;
}
}
};
数据流的中位数
题目描述
中位数是有序列表中间的数。如果列表长度是偶数,中位数则是中间两个数的平均值。
例如,
[2,3,4]
的中位数是 3
[2,3]
的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
-
void addNum(int num) - 从数据流中添加一个整数到数据结构中。
-
double findMedian() - 返回目前所有元素的中位数。
示例:
addNum(1)
addNum(2)
findMedian() -> 1.5
addNum(3)
findMedian() -> 2
进阶:
-
如果数据流中所有整数都在 0 到 100 范围内,你将如何优化你的算法?
-
如果数据流中 99% 的整数都在 0 到 100 范围内,你将如何优化你的算法?
解法
维护一个大根堆 bigRoot 和一个小根堆 smallRoot,若有 n 个数,较小的 n/2 个数放在大根堆,而较大的 n/2 个数放在小根堆。
-
若其中一个堆的元素个数比另一个堆的元素个数大 1,弹出堆顶元素到另一个堆。
-
获取中位数时,若两个堆的元素个数相等,返回两个堆堆顶的平均值。否则返回元素个数较多的堆的堆顶。
class MedianFinder {
private PriorityQueue<Integer> bigRoot;
private PriorityQueue<Integer> smallRoot;
/** initialize your data structure here. */
public MedianFinder() {
bigRoot = new PriorityQueue<>(Comparator.reverseOrder());
smallRoot = new PriorityQueue<>(Integer::compareTo);
}
public void addNum(int num) {
if (bigRoot.isEmpty() || bigRoot.peek() > num) {
bigRoot.offer(num);
} else {
smallRoot.offer(num);
}
int size1 = bigRoot.size();
int size2 = smallRoot.size();
if (size1 - size2 > 1) {
smallRoot.offer(bigRoot.poll());
} else if (size2 - size1 > 1) {
bigRoot.offer(smallRoot.poll());
}
}
public double findMedian() {
int size1 = bigRoot.size();
int size2 = smallRoot.size();
return size1 == size2
? (bigRoot.peek() + smallRoot.peek()) * 1.0 / 2
: (size1 > size2 ? bigRoot.peek() : smallRoot.peek());
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
最长上升子序列
题目描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
-
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
-
你算法的时间复杂度应该为
O(n²)
。
进阶: 你能将算法的时间复杂度降低到 O(n log n)
吗?
解法
记以 nums[i]
结尾的最长递增子序列长度为 res[i]
。
判断 nums[j](0 < j < i)
中是否满足 nums[j] < nums[i]
-
若是,
res[i + 1]
可更新为res[j] + 1
。求出最大值,即为当前res[i + 1]
的值。 -
若不满足,
res[i + 1] = 1
求解过程中,也求出 res
的最大值,即为最长的子序列的长度。
class Solution {
public int lengthOfLIS(int[] nums) {
if (nums == null) {
return 0;
}
int n = nums.length;
if (n < 2) {
return n;
}
int[] res = new int[n];
res[0] = 1;
res[1] = nums[1] > nums[0] ? 2 : 1;
int max = res[1];
for (int i = 2; i < n; ++i) {
res[i] = 1;
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
res[i] = Math.max(res[i], res[j] + 1);
}
}
max = Math.max(max, res[i]);
}
return max;
}
}
戳气球
题目描述
有 n
个气球,编号为 0
到 n-1
,每个气球上都标有一个数字,这些数字存在数组 nums
中。
现在要求你戳破所有的气球。每当你戳破一个气球 i
时,你可以获得 nums[left] * nums[i] * nums[right]
个硬币。 这里的 left
和 right
代表和 i
相邻的两个气球的序号。注意当你戳破了气球 i
后,气球 left
和气球 right
就变成了相邻的气球。
求所能获得硬币的最大数量。
说明:
-
你可以假设
nums[-1] = nums[n] = 1
,但注意它们不是真实存在的所以并不能被戳破。 -
0 ≤
n
≤ 500, 0 ≤nums[i]
≤ 100
示例:
输入: [3,1,5,8]
输出: 167
解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
解法
数组 f
表示某范围内(开区间)戳破所有气球能获得的最大硬币。那么题意转换为求f[0][n+1]
。
假设最后一个戳破的气球为 i
,那么:
f[0][n+1] = f[0][i] + f[i][n+1] + nums[i] * nums[0][n+1]。
利用记忆化搜索,遍历当最后一个戳破的气球为 i
时,求 f[0][n+1]
的最大值。
class Solution {
public int maxCoins(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int n = nums.length;
int[][] f = new int[n + 2][n + 2];
for (int i= 0; i < n + 2; ++i) {
for (int j = 0; j < n + 2; ++j) {
f[i][j] = -1;
}
}
int[] bak = new int[n + 2];
bak[0] = bak[n + 1] = 1;
for (int i = 1; i < n + 1; ++i) {
bak[i] = nums[i - 1];
}
return dp(bak, f, 0, n + 1);
}
private int dp(int[] nums, int[][] f, int x, int y) {
if (f[x][y] != -1) {
return f[x][y];
}
f[x][y] = 0;
//枚举最后一个戳破的气球的位置
for (int i = x + 1; i < y; ++i) {
f[x][y] = Math.max(f[x][y], nums[i] * nums[x] * nums[y] + dp(nums,f, x, i) + dp(nums, f, i, y));
}
return f[x][y];
}
}
奇偶链表
题目描述
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
示例 2:
输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
说明:
-
应当保持奇数节点和偶数节点的相对顺序。
-
链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
解法
利用 pre
, cur
指针指向链表的第 1、2 个结点,循环:将 cur
的下一个结点挂在 pre
的后面,pre
和 cur
指针向右移动。
eg.
1->2->3->4->5: `pre`指向 1,`cur`指向 2
第一次循环后:1->3->2->4->5: `pre`指向 2, `cur`指向 4
...
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode oddEvenList(ListNode head) {
// 链表结点数小于 3,直接返回
if (head == null || head.next == null || head.next.next == null) {
return head;
}
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = head, t = pre.next, cur = t;
while (cur != null && cur.next != null) {
pre.next = cur.next;
cur.next = cur.next.next;
pre.next.next = t;
pre = pre.next;
// cur.next可能为空,所以在下一次循环要判断 cur!= null 是否满足
cur = cur.next;
t = pre.next;
}
return dummy.next;
}
}
342. Power of Four
Given an integer (signed 32 bits), write a function to check whether it is a power of 4.
Example 1:
Input: 16
Output: true
Example 2:
Input: 5
Output: false
Follow up: Could you solve it without loops/recursion?
整数拆分
题目描述
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
说明: 你可以假设 n 不小于 2 且不大于 58。
解法
本题利用贪心法求解。
当 n>=5 时,尽可能多地拆分为 3。当剩下的正数为 4 时,把正数拆分为两个 2。
证明:
首先,当 n>=5 时,可以证明 2(n-2)>n,并且 3(n-3)>n。也就是说,数字大于等于 5 时,把它拆分为数字 3 或者 2。
另外,当 n>=5 时,3(n-3)>=2(n-2)。因此,尽可能多地拆分为整数 3。
当 n=4 时,其实没必要拆分,只是题目要求至少要拆分为两个整数。那么就拆分为两个 2。
class Solution {
public int integerBreak(int n) {
if (n < 2) {
return 0;
}
if (n < 4) {
return n - 1;
}
int timesOf3 = n / 3;
if (n % 3 == 1) {
--timesOf3;
}
int timesOf2 = (n - timesOf3 * 3) >> 1;
return (int) (Math.pow(2, timesOf2) * Math.pow(3, timesOf3));
}
}
反转字符串
题目描述
编写一个函数,其作用是将输入的字符串反转过来。
示例 1:
输入: "hello"
输出: "olleh"
示例 2:
输入: "A man, a plan, a canal: Panama"
输出: "amanaP :lanac a ,nalp a ,nam A"
解法
本题利用双指针解决。
class Solution {
public String reverseString(String s) {
if (s == null || s.length() < 2) {
return s;
}
char[] chars = s.toCharArray();
int p = 0;
int q = chars.length - 1;
while (p < q) {
char tmp = chars[p];
chars[p] = chars[q];
chars[q] = tmp;
++p;
--q;
}
return String.valueOf(chars);
}
}
字符串解码
问题描述
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要
求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像
3a
或 2[4]
的输入。
示例:
s = "3[a]2[bc]", 返回 "aaabcbc".
s = "3[a2[c]]", 返回 "accaccacc".
s = "2[abc]3[cd]ef", 返回 "abcabccdcdcdef".
解法
递归求解。设所求为ans
。遍历字符串,每次遇到字母,则加入ans
;每次遇到数字,则存下;
遇到[
,则找到与其匹配的]
,并个位置之间的进行递归求解,并将所得结果乘以遇到的数字,
再加入ans
。
class Solution(object):
def decodeString(self, s):
def deco(s):
if '[' not in s and ']' not in s:
return s
i = j = 0
ans = ''
count = ''
while i < len(s):
if s[i].isdigit():
count += s[i]
i += 1
elif s[i].isalpha():
ans += s[i]
i += 1
elif s[i] == '[':
j = i + 1
zuo = 0
while j < len(s):
if s[j] == '[':
zuo += 1
j += 1
elif s[j] == ']':
if zuo != 0:
zuo -= 1
j += 1
else:
if not count:
ans += deco(s[i + 1:j])
else:
ans += int(count) * deco(s[i + 1:j])
count = ''
i = j + 1
break
else:
j += 1
return ans
return deco(s)
数组中两个数的最大异或值
题目描述
给定一个非空数组,数组中元素为 a0, a1, a2, … , an-1,其中 0 ≤ ai < 231 。
找到 ai 和aj 最大的异或 (XOR) 运算结果,其中0 ≤ i, j < n 。
示例:
输入: [3, 10, 5, 25, 2, 8]
输出: 28
解释: 最大的结果是 5 ^ 25 = 28.
解法
异或运算被称为不做进位的二进制加法运算, 且具有一个性质:如果 a ^ b = c 成立,那么a ^ c = b 与 b ^ c = a 均成立。
分析一下题目, 要在数组中找到两个数对他们进行异或运算后得到一个最大的异或值, 即这个异或值二进制表示非 0 最高位要尽可能的靠左同时剩余位尽可能为 1;
整体使用贪心原则, 依次假设整数从左至右第 i 为 1, 然后再使用一个 mask 与数组中所有数相与得到数据前 i 位的一个前缀集合, 再把之前一次 i-1
循环所得到的 max 加第 i 位;为 1 得到当前 i 循环中期望的 pre-max
, 再与前缀集合中的所有数进行异或运算, 如果得到的值也同时在集合中, 表示假设成立, max
变为 pre-max
, 否则直接 i+1
进行下一个循环, 直到 i=0
算法结束。
class Solution {
public int findMaximumXOR(int[] numbers) {
int max = 0;
int mask = 0;
for (int i = 30; i >= 0; i--) {
int current = 1 << i;
// 期望的二进制前缀
mask = mask ^ current;
// 在当前前缀下, 数组内的前缀位数所有情况集合
Set<Integer> set = new HashSet<>();
for (int j = 0, k = numbers.length; j < k; j++) {
set.add(mask & numbers[j]);
}
// 期望最终异或值的从右数第i位为1, 再根据异或运算的特性推算假设是否成立
int flag = max | current;
for (Integer prefix : set) {
if (set.contains(prefix ^ flag)) {
max = flag;
break;
}
}
}
return max;
}
}
找到字符串中所有字母异位词
问题描述
给定一个字符串 s
和一个非空字符串 p
,找到 s
中所有是 p
的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s
和 p
的长度都不超过 20100。
示例1:
输入:
s: "cbaebabacd" p: "abc"
输出:
[0, 6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。
示例2:
输入:
s: "abab" p: "ab"
输出:
[0, 1, 2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的字母异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的字母异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的字母异位词。
提示:
-
字母异位词指字母相同,但排列不同的字符串。
-
不考虑答案输出的顺序。
解法
同第0567
题。
使用滑动窗口。窗口宽为p
的长度,从左到右在s
上滑动,若窗口内的字母出现频率和p
中字母出现的频率一样,则此窗口的起始位置为一个起始索引。
class Solution:
def findAnagrams(self, s, p):
lens = len(s)
lenp = len(p)
if lens < lenp:
return []
flag1 = [0] * 26
flag2 = [0] * 26
for i, x in enumerate(p):
flag1[ord(x) - 97] += 1
ans = []
for i, x in enumerate(s):
flag2[ord(x) - 97] += 1
if i >= lenp:
flag2[ord(s[i - lenp]) - 97] -= 1
if flag1 == flag2:
ans.append(i - lenp + 1)
return ans
找到所有数组中消失的数字
问题描述
给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 )
的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。
找到所有在 [1, n] 范围之间没有出现在数组中的数字。
您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。
示例:
输入:
[4,3,2,7,8,2,3,1]
输出:
[5,6]
解法
题目要求不使用额外空间,所以计数排序的方法不可取;
根据题目的条件,给定的a[i]
是在[1,n]
的范围内,所以可以利用这种关系来对数组进行处理,如a[a[i]] = -a[a[i]
作反标记,最终若a[i]>0
的话,则证明该下标i
没有出现过,加入输出数组
class Solution {
public:
vector<int> findDisappearedNumbers(vector<int>& nums) {
int len = nums.size();
vector<int> ans;
if(len == 0)return ans;
int index;
for(int i = 0;i<len;++i){
index = abs(nums[i]) - 1;
if(nums[index] > 0)
nums[index] = -nums[index];
}
for(int i = 0;i<len;++i){
if(nums[i] > 0)
ans.push_back(i+1);
}
return ans;
}
};
环形数组循环
题目描述
给定一个含有正整数和负整数的环形数组 nums
。 如果某个索引中的数 k 为正数,则向前移动 k 个索引。相反,如果是负数 (-k),则向后移动 k 个索引。因为数组是环形的,所以可以假设最后一个元素的下一个元素是第一个元素,而第一个元素的前一个元素是最后一个元素。
确定 nums 中是否存在循环(或周期)。循环必须在相同的索引处开始和结束并且循环长度 > 1。此外,一个循环中的所有运动都必须沿着同一方向进行。换句话说,一个循环中不能同时包括向前的运动和向后的运动。
示例 1
输入:[2,-1,1,2,2]
输出:true
解释:存在循环,按索引 0 -> 2 -> 3 -> 0 。循环长度为 3 。
示例 2
输入:[-1,2]
输出:false
解释:按索引 1 -> 1 -> 1 ... 的运动无法构成循环,因为循环的长度为 1 。根据定义,循环的长度必须大于 1 。
示例 3
输入:[-2,1,-1,-2,-2]
输出:false
解释:按索引 1 -> 2 -> 1 -> ... 的运动无法构成循环,因为按索引 1 -> 2 的运动是向前的运动,而按索引 2 -> 1 的运动是向后的运动。一个循环中的所有运动都必须沿着同一方向进行。
提示
-
-1000 ≤ nums[i] ≤ 1000
-
nums[i] ≠ 0
-
1 ≤ nums.length ≤ 5000
进阶
你能写出时间时间复杂度为 O(n) 和额外空间复杂度为 O(1) 的算法吗?
解题思路
思路
若时间复杂度为 O(n^2) 则不用记录,双重遍历就完事了。降低时间复杂度为 O(n) 首先想到设置一个visit
数组来记录是否被访问。若要空间复杂度为 O(1) ,考虑-1000 ≤ nums[i] ≤ 1000
,可以用大于1000或小于-1000来记录是否被访问。需要注意的是循环长度要大于1
,在一条链路中可能会出现0 -> 2 -> 3 -> 3
这种在尾部存在的自循环。
算法
python
class Solution:
def circularArrayLoop(self, nums: 'List[int]') -> 'bool':
flag = 1000
lent = len(nums)
drt = 1 # -1->left 1->right
for loc in range(lent):
if nums[loc] > 1000:
continue
if nums[loc] < 0:
drt = -1
else:
drt = 1
ct = (loc + nums[loc]) % lent
flag += 1
nums[loc] = flag
start = flag
tmp = ct
while -1000 <= nums[ct] <= 1000:
if nums[ct] * drt < 0:
break
tmp = ct
ct = (ct + nums[ct]) % lent
flag += 1
nums[tmp] = flag
else:
if nums[ct] != nums[tmp] and nums[ct] >= start:
return True
return False
斐波那契数
题目描述
斐波那契数,通常用 F(n)
表示,形成的序列称为斐波那契数列。该数列由 0
和 1
开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
给定 N
,计算 F(N)
。
示例 1:
输入:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1.
示例 2:
输入:3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2.
示例 3:
输入:4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3.
提示:
- 0 ≤
N
≤ 30
解题思路
思路
利用递归思想,递归公式F(n) = F(n - 1) + F(n - 2)
,递归的终止条件F(1) = 1
,F(2) = 2
。
算法
let preResult = {};
var fib = function(N) {
if( N === 0 ) return 0;
if( N === 1 ) return 1;
if( preResult[N] ){
return preResult[N];
} else {
preResult[N] = fib(N-1) + fib(N-2);
}
return fib(N-1) + fib(N-2);
};
复杂度分析
暂无
最长特殊序列 Ⅰ
问题描述
给定两个字符串,你需要从这两个字符串中找出最长的特殊序列。最长特殊序列定义如下:该序列为某字符串独有的最长子序列(即不能是其他字符串的子序列)。
子序列可以通过删去字符串中的某些字符实现,但不能改变剩余字符的相对顺序。空序列为所有字符串的子序列,任何字符串为其自身的子序列。
输入为两个字符串,输出最长特殊序列的长度。如果不存在,则返回 -1
。
示例:
输入: "aba", "cdc"
输出: 3
解析: 最长特殊序列可为 "aba" (或 "cdc")
提示:
-
两个字符串长度均小于100。
-
字符串中的字符仅含有 'a'~'z'。
解法
若a
与b
相等,则不存在最长子序列;若不相等,则其中较长的就是就是最长子序列。
class Solution:
def findLUSlength(self, a, b):
return -1 if a == b else max(len(a), len(b))
砖墙
问题描述
你的面前有一堵方形的、由多行砖块组成的砖墙。 这些砖块高度相同但是宽度不同。你现在要画一条自顶向下的、穿过最少砖块的垂线。
砖墙由行的列表表示。 每一行都是一个代表从左至右每块砖的宽度的整数列表。
如果你画的线只是从砖块的边缘经过,就不算穿过这块砖。你需要找出怎样画才能使这条线穿过的砖块数量最少,并且返回穿过的砖块数量。
你不能沿着墙的两个垂直边缘之一画线,这样显然是没有穿过一块砖的。
示例1:
输入: [[1,2,2,1],
[3,1,2],
[1,3,2],
[2,4],
[3,1,2],
[1,3,1,1]]
输出: 2
提示:
-
每一行砖块的宽度之和应该相等,并且不能超过
INT_MAX
。 -
每一行砖块的数量在 [1,10,000] 范围内, 墙的高度在 [1,10,000] 范围内, 总的砖块数量不超过 20,000。
解法
记录每个长度(每个砖块右侧到最左侧的长度)出现的次数, 总层数减去出现次数最多的长度, 就是最少交叉的数量。每层最后一块砖的右侧是边缘,不考虑。
class Solution:
def leastBricks(self, wall):
dic = {}
count = 0
for hang in wall:
count = 0
for j, ele in enumerate(hang):
if j == len(hang) - 1:
break
count += ele
if count not in dic:
dic[count] = 1
else:
dic[count] += 1
if not dic:
return len(wall)
return len(wall) - dic[max(dic, key=dic.get)]
数组拆分 I
题目描述
给定长度为 2n 的数组, 你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), ..., (an, bn) ,使得从1 到 n 的 min(ai, bi) 总和最大。
示例 1
输入: [1,4,3,2]
输出: 4
解释: n 等于 2, 最大总和为 4 = min(1, 2) + min(3, 4).
提示
-
n 是正整数,范围在 [1, 10000].
-
数组中的元素范围在 [-10000, 10000].
提示1
Obviously, brute force won't help here. Think of something else, take some example like 1,2,3,4.
显然,蛮力在这里无济于事。想想其他的东西,比如1,2,3,4。
提示2
How will you make pairs to get the result? There must be some pattern.
你将如何成对获得结果? 必须有一些模式。
提示3
Did you observe that- Minimum element gets add into the result in sacrifice of maximum element.
您是否观察到 - 最小元素会牺牲最大元素而添加到结果中。
提示4
Still won't able to find pairs? Sort the array and try to find the pattern.
仍然无法找到对? 对数组进行排序并尝试查找模式。
解题思路
思路
先排序,在查找配对的两个数中最小值求和。这里使用的是归并排序和根据数组下标为奇数求和,有一些取巧,但有更快的算法(例如算法里使用C的解法,复杂度为O(n)),但还没想明白。
算法
JavaScript
var arrayPairSum = function(nums) {
let result = merge_sort(nums);
let sum = 0;
for ( let i = 0; i < result.length; i += 2 ) {
sum += result[i];
}
return sum;
};
function merge_sort(A){
let len = A.length;
if ( len === 1 ) return A;
let mid = ~~( len / 2 );
let leftArray = A.slice(0, mid);
let rightArray = A.slice(mid, len);
return merge(merge_sort(leftArray), merge_sort(rightArray));
}
function merge(left, right){
let i = 0, j = 0, k = 0, tmp = [];
while ( i < left.length && j < right.length ) {
if ( left[i] <= right[j] ) {
tmp[k++] = left[i++];
} else {
tmp[k++] = right[j++];
}
}
while ( i < left.length ) {
tmp[k++] = left[i++];
}
while ( j < right.length ) {
tmp[k++] = right[j++];
}
return tmp;
}
C
nt arrayPairSum(vector<int>& nums) {
if( nums.size() == 0)
return 0;
int min = nums[0], max= nums[0];
for( int i = 0; i < nums.size(); i++)
if( nums[i] < min)
min = nums[i];
else if( nums[i] > max)
max = nums[i];
int *record = (int *)malloc( sizeof( int) * ( max - min +1));
for( int i = 0; i< max - min + 1; i++)
record[i] = 0;
for( int i = 0; i < nums.size(); i++)
record[ nums[i] - min]++;
int maxSum = 0, i = 0;
while(i < max-min+1){
if( record[i] == 0){
i++;
continue;
}
if( record[i] > 1){
maxSum += i + min;
record[i] -=2;
continue;
}
if( record[i] == 1){
for( int j = i+1; j < max-min+1; j++)
if( record[j] != 0){
record[j]--, record[i]--;
maxSum += i + min;
i = j;
break;
}
continue;
}
}
return maxSum;
}
复杂度分析
暂无
字符串的排列
问题描述
给定两个字符串 s1
和 s2
,写一个函数来判断 s2
是否包含 s1
的排列。
换句话说,第一个字符串的排列之一是第二个字符串的子串。
示例1:
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
示例2:
输入: s1= "ab" s2 = "eidboaoo"
输出: False
提示:
-
输入的字符串只包含小写字母
-
两个字符串的长度都在 [1, 10,000] 之间
解法
使用滑动窗口,窗口宽为s1
的长度,从左到右在s2
上滑动,若窗口内的字母出现频率和s1
中字母出现的频率一样,则s2
包含 s1
的排列。若不存在则s2
不包含 s1
的排列。
class Solution:
def checkInclusion(self, s1, s2):
if len(s1) > len(s2):
return False
flag1 = [0] * 26
flag2 = [0] * 26
for i, x in enumerate(s1):
flag1[ord(x) - ord('a')] += 1
for i, x in enumerate(s2):
flag2[ord(x) - ord('a')] += 1
if i >= len(s1):
flag2[ord(s2[i - len(s1)]) - ord('a')] -= 1
if flag1 == flag2:
return True
return False
最短无序连续子数组
题目描述
给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。
你找到的子数组应是最短的,请输出它的长度。
示例 1:
输入: [2, 6, 4, 8, 10, 9, 15]
输出: 5
解释: 你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。
说明 :
-
输入的数组长度范围在 [1, 10,000]。
-
输入的数组可能包含重复元素 ,所以升序的意思是<=。
解法
对数组排序,形成新数组,利用两个指针从头尾往中间遍历两数组,若对应位置的元素不同,则记录该下标,最后返回两个下标差。
class Solution {
public int findUnsortedSubarray(int[] nums) {
int n = nums.length;
if (n == 1) {
return 0;
}
int[] res = new int[n];
for (int i = 0; i < n; ++i) {
res[i] = nums[i];
}
Arrays.sort(res);
int p = 0;
for (; p < n; ++p) {
if (res[p] != nums[p]) {
break;
}
}
int q = n - 1;
for (; q >= 0; --q) {
if (res[q] != nums[q]) {
break;
}
}
return p == n ? 0 : q - p + 1 ;
}
}
大的国家
题目描述
这里有张 World
表
+-----------------+------------+------------+--------------+---------------+
| name | continent | area | population | gdp |
+-----------------+------------+------------+--------------+---------------+
| Afghanistan | Asia | 652230 | 25500100 | 20343000 |
| Albania | Europe | 28748 | 2831741 | 12960000 |
| Algeria | Africa | 2381741 | 37100000 | 188681000 |
| Andorra | Europe | 468 | 78115 | 3712000 |
| Angola | Africa | 1246700 | 20609294 | 100990000 |
+-----------------+------------+------------+--------------+---------------+
如果一个国家的面积超过300万平方公里,或者人口超过2500万,那么这个国家就是大国家。
编写一个SQL查询,输出表中所有大国家的名称、人口和面积。
例如,根据上表,我们应该输出:
+--------------+-------------+--------------+
| name | population | area |
+--------------+-------------+--------------+
| Afghanistan | 25500100 | 652230 |
| Algeria | 37100000 | 2381741 |
+--------------+-------------+--------------+
解法
# Write your MySQL query statement below
select distinct name, population, area from World where area > 3000000 or population > 25000000
超过5名学生的课
题目描述
有一个 courses
表 ,有: student(学生) 和 class(课程)。
请列出所有超过或等于5名学生的课。
例如,表:
+---------+------------+
| student | class |
+---------+------------+
| A | Math |
| B | English |
| C | Math |
| D | Biology |
| E | Math |
| F | Computer |
| G | Math |
| H | Math |
| I | Math |
+---------+------------+
应该输出:
+---------+
| class |
+---------+
| Math |
+---------+
Note:
学生在每个课中不应被重复计算。
解法
注意学生可能被重复计算,需要 distinct
。
# Write your MySQL query statement below
select class from courses group by class having count(distinct student) >= 5
种花问题
题目描述
假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给定一个花坛(表示为一个数组包含 0 和 1,其中 0 表示没种植花,1 表示种植了花),和一个数 n
。能否在不打破种植规则的情况下种入 n
朵花?能则返回 True
,不能则返回 False
。
示例 1:
输入: flowerbed = [1,0,0,0,1], n = 1
输出: True
示例 2:
输入: flowerbed = [1,0,0,0,1], n = 2
输出: False
注意:
-
数组内已种好的花不会违反种植规则。
-
输入的数组长度范围为 [1, 20000]。
-
n 是非负整数,且不会超过输入数组的大小。
解法
遍历数组,若当前位置的元素为 0 并且左右元素也为 0,则该位置可以种花,计数器 +1。
注意,需要特殊处理数组首尾元素。
class Solution {
public boolean canPlaceFlowers(int[] flowerbed, int n) {
int len = flowerbed.length;
int cnt = 0;
for (int i = 0; i < len; ++i) {
if (flowerbed[i] == 0 && (i == 0 || flowerbed[i - 1] == 0) && (i == len - 1 || flowerbed[i + 1] == 0)) {
++cnt;
flowerbed[i] = 1;
}
}
return cnt >= n;
}
}
有趣的电影
题目描述
某城市开了一家新的电影院,吸引了很多人过来看电影。该电影院特别注意用户体验,专门有个 LED显示板做电影推荐,上面公布着影评和相关电影描述。
作为该电影院的信息部主管,您需要编写一个 SQL查询,找出所有影片描述为非 boring
(不无聊) 的并且 id 为奇数 的影片,结果请按等级 rating
排列。
例如,下表 cinema
:
+---------+-----------+--------------+-----------+
| id | movie | description | rating |
+---------+-----------+--------------+-----------+
| 1 | War | great 3D | 8.9 |
| 2 | Science | fiction | 8.5 |
| 3 | irish | boring | 6.2 |
| 4 | Ice song | Fantacy | 8.6 |
| 5 | House card| Interesting| 9.1 |
+---------+-----------+--------------+-----------+
对于上面的例子,则正确的输出是为:
+---------+-----------+--------------+-----------+
| id | movie | description | rating |
+---------+-----------+--------------+-----------+
| 5 | House card| Interesting| 9.1 |
| 1 | War | great 3D | 8.9 |
+---------+-----------+--------------+-----------+
解法
简单查询即可。
# Write your MySQL query statement below
select id, movie, description, rating
from cinema
where description != 'boring' and mod(id, 2) = 1
order by rating desc;
Input
{"headers":{"cinema":["id", "movie", "description", "rating"]},"rows":{"cinema":[[1, "War", "great 3D", 8.9], [2, "Science", "fiction", 8.5], [3, "irish", "boring", 6.2], [4, "Ice song", "Fantacy", 8.6], [5, "House card", "Interesting", 9.1]]}}
Output
{"headers":["id","movie","description","rating"],"values":[[5,"House card","Interesting",9.1],[1,"War","great 3D",8.9]]}
交换工资
题目描述
给定一个 salary
表,如下所示,有m=男性 和 f=女性的值 。交换所有的 f 和 m 值(例如,将所有 f 值更改为 m,反之亦然)。要求使用一个更新查询,并且没有中间临时表。
例如:
| id | name | sex | salary |
|----|------|-----|--------|
| 1 | A | m | 2500 |
| 2 | B | f | 1500 |
| 3 | C | m | 5500 |
| 4 | D | f | 500 |
运行你所编写的查询语句之后,将会得到以下表:
| id | name | sex | salary |
|----|------|-----|--------|
| 1 | A | f | 2500 |
| 2 | B | m | 1500 |
| 3 | C | f | 5500 |
| 4 | D | m | 500 |
解法
使用 if
函数 或者 case when .. then .. else ... end
。
# Write your MySQL query statement below
# update salary
# set sex = if(sex = 'm', 'f', 'm')
update salary
set sex = (case when sex = 'f' then 'm' else 'f' end)
Input
{"headers":{"salary":["id","name","sex","salary"]},"rows":{"salary":[[1,"A","m",2500],[2,"B","f",1500],[3,"C","m",5500],[4,"D","f",500]]}}
Output
{"headers":["id","name","sex","salary"],"values":[[1,"A","f",2500],[2,"B","m",1500],[3,"C","f",5500],[4,"D","m",500]]}
非递减数列
问题描述
给定一个长度为 n
的整数数组,你的任务是判断在最多改变 1
个元素的情况下,该数组能否变成一个非递减数列。
我们是这样定义一个非递减数列的: 对于数组中所有的 i(1 <= i < n)
,满足 array[i] <= array[i + 1]
。
示例1:
输入: [4,2,3]
输出: True
解释: 你可以通过把第一个4变成1来使得它成为一个非递减数列。
示例2:
输入: [4,2,1]
输出: False
解释: 你不能在只改变一个元素的情况下将其变为非递减数列。
提示:
n
的范围为[1, 10,000]
。
解法
若数组中不存在下降的点,则其为非递减数列;若数组中存在两个下降的点,则一定不能通过改变1
个元素来变成一个非递减数列;若数组中只存在一个下降的点,其位置为x
,则其能通过改变1
个元素来变成非递减数列需要满足下列任一要求:
-
x
为最右的元素; -
x
为第二个元素; -
nums[x + 1] >= nums[x - 1]
; -
nums[i - 2] < nums[i]
。
class Solution:
def checkPossibility(self, nums):
if len(nums) < 2:
return True
count = 0
for i in range(1, len(nums)):
if nums[i] < nums[i - 1]:
if count == 1:
return False
if not(i + 1 == len(nums) or nums[i + 1] >= nums[i - 1] or i - 2 < 0 or nums[i - 2] < nums[i]):
return False
else:
count = 1
return True
最大交换
问题描述
给定一个非负整数,你至多可以交换一次数字中的任意两位。返回你能得到的最大值。
示例1:
输入: 2736
输出: 7236
解释: 交换数字2和数字7。
示例2:
输入: 9973
输出: 9973
解释: 不需要交换。
注意:
- 给定数字的范围是 [0, 108]
解法
当成字符串处理。若可以随意排列,可以得到的最大数(即为反字典顺序)val与原来的数字val0第一次不相同的位置即为需要交换的位置,同时可知需要交换的数字。再从个位数依次向左查找被交换的位置。
示例:
输入: 2736
最大数val:7632
交换位置:0
交换数字:2,7
被交换位置:1
输出: 7236
解释: 交换数字2和数字7。
class Solution:
def maximumSwap(self, num):
# s为能得到的最大数
s = ''.join(sorted(list(str(num)), reverse=True))
nums = str(num)
if s == nums:
return num
for i in range(len(s)):
if s[i] != nums[i]:
kai = i
break
for i in range(len(nums) - 1, -1, -1):
if nums[i] == s[kai]:
loc = i
break
return int(s[:kai + 1] + nums[kai + 1:loc] + nums[kai] + nums[loc + 1:])
岛屿的最大面积
题目描述
给定一个包含了一些 0 和 1的非空二维数组 grid
, 一个 岛屿 是由四个方向 (水平或垂直) 的 1
(代表土地) 构成的组合。你可以假设二维矩阵的四个边缘都被水包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为0。)
示例 1:
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
对于上面这个给定矩阵应返回 6
。注意答案不应该是 11,因为岛屿只能包含水平或垂直的四个方向的‘1’。
示例 2:
[[0,0,0,0,0,0,0,0]]
对于上面这个给定的矩阵, 返回 0
。
注意: 给定的矩阵 grid
的长度和宽度都不超过 50。
解法
利用深度优先搜索,遍历矩阵,搜索每个位置上下左右四个方向,若为 1,则累加 1 并将其置为 0 后继续搜索。最后返回最大值。
class Solution {
public int maxAreaOfIsland(int[][] grid) {
int res = 0;
int m = grid.length;
int n = grid[0].length;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
res = Math.max(res, dfs(grid, i, j, m, n));
}
}
return res;
}
private int dfs(int[][] grid, int i, int j, int m, int n) {
if (i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == 0) {
return 0;
}
grid[i][j] = 0;
return 1
+ dfs(grid, i - 1, j, m, n)
+ dfs(grid, i + 1, j, m, n)
+ dfs(grid, i, j - 1, m, n)
+ dfs(grid, i, j + 1, m, n);
}
}
二叉搜索树中的插入操作
题目描述
给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 保证原始二叉搜索树中不存在新值。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。
示例 :
给定二叉搜索树:
4
/ \
2 7
/ \
1 3
和 插入的值: 5
你可以返回这个二叉搜索树:
4
/ \
2 7
/ \ /
1 3 5
或者这个树也是有效的:
5
/ \
2 7
/ \
1 3
\
4
解法一
返回第一种类型。遍历每个节点,若节点值大于插入值,搜索其左子节点,若小于插入值,则搜索其右子节点。若其节点不存在,则该位置为需要插入的值所在节点。使用递归会使运行时间相对增加,而循环语句的运行更快。
class Solution:
def insertIntoBST(self, root, val):
if not root:
return TreeNode(val)
if root.val < val:
if root.right:
self.insertIntoBST(root.right, val)
else:
root.right = TreeNode(val)
if root.val > val:
if root.left:
self.insertIntoBST(root.left, val)
else:
root.left = TreeNode(val)
return root
解法二(无法AC)
返回第二种类型。先将根节点替换为插入值,再将根节点的值放到其左子节点中的最右子节点。但是这种解法无法AC,个人认为是题目并不支持返回第二种类型的结果。
class Solution:
def insertIntoBST(self, root, val):
if not root:
return TreeNode(val)
elif root.left is None:
root.left = TreeNode(root.val)
root.val = val
root.val, val = val, root.val
node = root.left
while node.right:
node = node.right
node.right = TreeNode(val)
return root
数据流中的第K大元素
题目描述
设计一个找到数据流中第 K 大元素的类(class)。注意是排序后的第 K 大元素,不是第 K 个不同的元素。
你的 KthLargest
类需要一个同时接收整数 k
和整数数组nums
的构造器,它包含数据流中的初始元素。每次调用 KthLargest.add
,返回当前数据流中第 K 大的元素。
示例:
int k = 3;
int[] arr = [4,5,8,2];
KthLargest kthLargest = new KthLargest(3, arr);
kthLargest.add(3); // returns 4
kthLargest.add(5); // returns 5
kthLargest.add(10); // returns 5
kthLargest.add(9); // returns 8
kthLargest.add(4); // returns 8
说明:
你可以假设 nums
的长度≥ k-1
且 k
≥ 1。
解法
建立一个有 k 个元素的小根堆。add 操作时:
-
若堆元素少于 k 个,直接添加到堆中;
-
若堆元素有 k 个,判断堆顶元素 peek() 与 val 的大小关系:若 peek() >= val,直接返回堆顶元素;若 peek() < val,弹出堆顶元素,将 val 添加至堆中,然后返回堆顶。
class KthLargest {
private PriorityQueue<Integer> minHeap;
private int size;
public KthLargest(int k, int[] nums) {
minHeap = new PriorityQueue<>(k);
size = k;
for (int e : nums) {
add(e);
}
}
public int add(int val) {
if (minHeap.size() < size) {
minHeap.add(val);
} else {
if (minHeap.peek() < val) {
minHeap.poll();
minHeap.add(val);
}
}
return minHeap.peek();
}
}
/**
* Your KthLargest object will be instantiated and called as such:
* KthLargest obj = new KthLargest(k, nums);
* int param_1 = obj.add(val);
*/
设计链表
题目描述
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev
以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
-
get(index):获取链表中第
index
个节点的值。如果索引无效,则返回-1
。 -
addAtHead(val):在链表的第一个元素之前添加一个值为
val
的节点。插入后,新节点将成为链表的第一个节点。 -
addAtTail(val):将值为
val
的节点追加到链表的最后一个元素。 -
addAtIndex(index,val):在链表中的第
index
个节点之前添加值为val
的节点。如果index
等于链表的长度,则该节点将附加到链表的末尾。如果index
大于链表长度,则不会插入节点。 -
deleteAtIndex(index):如果索引
index
有效,则删除链表中的第index
个节点。
示例:
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3
linkedList.get(1); //返回2
linkedList.deleteAtIndex(1); //现在链表是1-> 3
linkedList.get(1); //返回3
提示:
-
所有值都在
[1, 1000]
之内。 -
操作次数将在
[1, 1000]
之内。 -
请不要使用内置的 LinkedList 库。
解法
本题是链表基础,利用 size
记录链表长度。dummy
为虚拟节点,指向链表头节点。
class MyLinkedList {
private class Node {
int val;
Node next;
Node(int val) {
this.val = val;
}
}
private Node dummy;
private int size;
/** Initialize your data structure here. */
public MyLinkedList() {
dummy = new Node(-1);
size = 0;
}
/** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */
public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
Node cur = dummy;
for (int i = 0; i < index; ++i) {
cur = cur.next;
}
return cur.next.val;
}
/** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */
public void addAtHead(int val) {
Node node = new Node(val);
node.next = dummy.next;
dummy.next = node;
++size;
}
/** Append a node of value val to the last element of the linked list. */
public void addAtTail(int val) {
Node cur = dummy;
while (cur.next != null) {
cur = cur.next;
}
Node node = new Node(val);
cur.next = node;
++size;
}
/** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */
public void addAtIndex(int index, int val) {
if (index < 0 || index > size) {
return;
}
Node cur = dummy;
for (int i = 0; i < index; ++i) {
cur = cur.next;
}
Node node = new Node(val);
node.next = cur.next;
cur.next = node;
++size;
}
/** Delete the index-th node in the linked list, if the index is valid. */
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
Node cur = dummy;
for (int i = 0; i < index; ++i) {
cur = cur.next;
}
cur.next = cur.next.next;
--size;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
行星碰撞
问题描述
给定一个整数数组 asteroids,表示在同一行的行星。
对于数组中的每一个元素,其绝对值表示行星的大小,正负表示行星的移动方向(正表示向右移动,负表示向左移动)。每一颗行星以相同的速度移动。
找出碰撞后剩下的所有行星。碰撞规则:两个行星相互碰撞,较小的行星会爆炸。如果两颗行星大小相同,则两颗行星都会爆炸。两颗移动方向相同的行星,永远不会发生碰撞。
示例1:
输入:
asteroids = [5, 10, -5]
输出: [5, 10]
解释:
10 和 -5 碰撞后只剩下 10。 5 和 10 永远不会发生碰撞。
示例2:
输入:
asteroids = [8, -8]
输出: []
解释:
8 和 -8 碰撞后,两者都发生爆炸。
说明:
-
数组 asteroids 的长度不超过 10000。
-
每一颗行星的大小都是非零整数,范围是 [-1000, 1000] 。
解法
从左向右遍历数组。
-
对于遇到的大于零的小行星,应先假定其存活;
-
对于遇到的小于零的小行星
x
,其绝对值为'val',若之前没有大于零的小行星,则此小行星会存活。若之前有大于零的小行星,则从右到左遍历之前存活的小行星: -
遇到值小于零的小行星,则小行星
x
存活,并停止; -
遇到值等于'val'的小行星,那个小行星会爆炸,并停止;
-
遇到值大于'val'的小行星停止;
-
遇到值大于零小于'val'的小行星,那个小行星会爆炸,并继续向左遍历;
class Solution:
def asteroidCollision(self, asteroids):
if not asteroids:
return []
ans = []
for i in asteroids:
if i > 0:
ans.append(i)
if i < 0:
if not ans or ans[-1] < 0:
ans.append(i)
else:
while ans:
if ans[-1] < 0:
ans.append(i)
break
elif ans[-1] > abs(i):
break
elif ans[-1] == abs(i):
ans.pop(-1)
break
else:
ans.pop(-1)
else:
ans.append(i)
return ans
找出变位映射
问题描述
给定两个列表 A and B,并且 B 是 A 的变位。B 是 A 的变位的意思是 B 由 A 中的元素随机排列生成。
我们希望找出一个从 A 到 B 的索引映射 P 。一个映射 P[i] = j 的意思是 A 中的第 i 个元素出现于 B 中的第 j 个元素上。
列表 A 和 B 可能出现重复元素。如果有多于一种答案,输出任意一种。
例如,给定
A = [12, 28, 46, 32, 50]
B = [50, 12, 32, 46, 28]
需要返回
[1, 4, 3, 2, 0]
P[0] = 1 ,因为 A 中的第 0 个元素出现于 B[1],而且 P[1] = 4 因为 A 中第 1个元素出现于 B[4],以此类推。
注:
-
A, B 有相同的长度,范围为 [1, 100]。
-
A[i], B[i] 都是范围在 [0, 10^5] 的整数。
解法
先将 B 转换为 map,其中 key 为 B 中元素,value 为 B 对应元素的下标(索引位置)。之后遍历 A,读取每个元素在 map 中的 value 值,存到结果列表中。
- Java 解法
import java.util.*;
class Solution {
public int[] anagramMappings(int[] A, int[] B) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < B.length; i++) {
map.put(B[i], i);
}
int[] res = new int[B.length];
int j = 0;
for (int k : A) {
res[j++] = map.get(k);
}
return res;
}
}
- Python 解法
class Solution:
def anagramMappings(self, A, B):
"""
:type A: List[int]
:type B: List[int]
:rtype: List[int]
"""
record = {val: i for i, val in enumerate(B)}
return [record[val] for val in A]
二进制表示中质数个计算置位
问题描述
给定两个整数 L 和 R ,找到闭区间 [L, R] 范围内,计算置位位数为质数的整数个数。
(注意,计算置位代表二进制表示中1的个数。例如 21 的二进制表示 10101 有 3 个计算置位。还有,1 不是质数。)
示例1:
输入: L = 6, R = 10
输出: 4
解释:
6 -> 110 (2 个计算置位,2 是质数)
7 -> 111 (3 个计算置位,3 是质数)
9 -> 1001 (2 个计算置位,2 是质数)
10-> 1010 (2 个计算置位,2 是质数)
示例2:
输入: L = 10, R = 15
输出: 5
解释:
10 -> 1010 (2 个计算置位, 2 是质数)
11 -> 1011 (3 个计算置位, 3 是质数)
12 -> 1100 (2 个计算置位, 2 是质数)
13 -> 1101 (3 个计算置位, 3 是质数)
14 -> 1110 (3 个计算置位, 3 是质数)
15 -> 1111 (4 个计算置位, 4 不是质数)
提示:
-
L, R 是 L <= R 且在 [1, 10^6] 中的整数。
-
R - L 的最大值为 10000。
解法
列举32中的质数,得到ls
;遍历L
到R
之间的数,计算其计算置位的个数是否为质数(是否在ls
中)即可。
class Solution:
def countPrimeSetBits(self, L, R):
res = 0
flag = [2, 3, 5, 7, 11, 13, 17, 19]
for i in range(L, R + 1):
if bin(i).count('1') in flag:
res += 1
return res
最多能完成排序的块
问题描述
数组arr
是[0, 1, ..., arr.length - 1]
的一种排列,我们将这个数组分割成几个“块”,并将这些块分别进行排序。之后再连接起来,使得连接的结果和按升序排序后的原数组相同。
我们最多能将数组分成多少块?
示例1:
输入: arr = [4,3,2,1,0]
输出: 1
解释:
将数组分成2块或者更多块,都无法得到所需的结果。
例如,分成 [4, 3], [2, 1, 0] 的结果是 [3, 4, 0, 1, 2],这不是有序的数组。
示例2:
输入: arr = [1,0,2,3,4]
输出: 4
解释:
我们可以把它分成两块,例如 [1, 0], [2, 3, 4]。
然而,分成 [1, 0], [2], [3], [4] 可以得到最多的块数。
提示:
-
arr 的长度在 [1, 10] 之间。
-
arr[i]是 [0, 1, ..., arr.length - 1]的一种排列。
解法
从左到右,在位置i
处,只要位置i
及之前的位置不包括比i
大的值,就能分割成一个块。
class Solution:
def maxChunksToSorted(self, arr):
ans = 0
loc = 0
for i, val in enumerate(arr):
loc = val if loc < val else loc
if loc == i:
ans += 1
return ans
宝石与石头
问题描述
给定字符串 J
代表石头中宝石的类型,和字符串 S
代表你拥有的石头。 S
中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。
J
中的字母不重复,J
和 S
中的所有字符都是字母。字母区分大小写,因此"a"
和"A"
是不同类型的石头。
示例1:
输入: J = "aA", S = "aAAbbbb"
输出: 3
示例2:
输入: J = "z", S = "ZZ"
输出: 0
注意:
-
S
和J
最多含有 50 个字母。 -
J
中的字符不重复。
解法
题目中 J
字母不重复,因此,直接将 J
转换为 set。之后遍历 S
中每个字符,判断该字符是否在 set 中,若是则累加 1。最后得到累计后的结果。
- Java 解法
class Solution {
public int numJewelsInStones(String J, String S) {
Set<Character> set = new HashSet<>();
for (char ch : J.toCharArray()) {
set.add(ch);
}
int res = 0;
for (char ch : S.toCharArray()) {
res += (set.contains(ch) ? 1 : 0);
}
return res;
}
}
- Python 解法
class Solution:
def numJewelsInStones(self, J: str, S: str) -> int:
record = {ch for ch in J}
sum = 0
for ch in S:
sum += 1 if ch in record else 0
return sum
阻碍逃脱者
题目描述
你在进行一个简化版的吃豆人游戏。你从(0, 0)
点开始出发,你的目的地是 (target[0], target[1])
。地图上有一些阻碍者,第 i
个阻碍者从 (ghosts[i][0], ghosts[i][1])
出发。
每一回合,你和阻碍者们可以同时向东,西,南,北四个方向移动,每次可以移动到距离原位置1
个单位的新位置。
如果你可以在任何阻碍者抓住你之前到达目的地(阻碍者可以采取任意行动方式),则被视为逃脱成功。如果你和阻碍者同时到达了一个位置(包括目的地)都不算是逃脱成功。
当且仅当你有可能成功逃脱时,输出 True
。
示例 1:
输入:
ghosts = [[1, 0], [0, 3]]
target = [0, 1]
输出:true
解释:
你可以直接一步到达目的地(0,1),在(1, 0)或者(0, 3)位置的阻碍者都不可能抓住你。
示例 2:
输入:
ghosts = [[1, 0]]
target = [2, 0]
输出:false
解释:
你需要走到位于(2, 0)的目的地,但是在(1, 0)的阻碍者位于你和目的地之间。
示例 3:
输入:
ghosts = [[2, 0]]
target = [1, 0]
输出:false
解释:
阻碍者可以和你同时达到目的地。
解法
若你会被阻碍者抓住,则此阻碍者必定不会晚于你到达终点,因此我们只需要考虑阻碍者是否会比你晚到达终点即可,若存在不比你晚的,则返回false
,其他返回true
。
class Solution:
def escapeGhosts(self, ghosts, target):
flag=abs(target[0])+abs(target[1])
for i in ghosts:
if abs(i[0]-target[0])+abs(i[1]-target[1])<=flag:
return False
else:
return True
链表组件
题目描述
给定一个链表(链表结点包含一个整型值)的头结点 head。
同时给定列表 G
,该列表是上述链表中整型值的一个子集。
返回列表 G
中组件的个数,这里对组件的定义为:链表中一段最长连续结点的值(该值必须在列表 G
中)构成的集合。
示例1:
输入:
head: 0->1->2->3
G = [0, 1, 3]
输出: 2
解释:
链表中,0 和 1 是相连接的,且 G 中不包含 2,所以 [0, 1] 是 G 的一个组件,同理 [3] 也是一个组件,故返回 2。
示例2:
输入:
head: 0->1->2->3->4
G = [0, 3, 1, 4]
输出: 2
解释:
链表中,0 和 1 是相连接的,3 和 4 是相连接的,所以 [0, 1] 和 [3, 4] 是两个组件,故返回 2。
注意:
-
如果 N 是给定链表 head 的长度,1 <= N <= 10000。
-
链表中每个结点的值所在范围为 [0, N - 1]。
-
1 <= G.length <= 10000
-
G 是链表中所有结点的值的一个子集.
解法
链表被其中节点值不在列表G
中的节点分成组件。依次遍历链表中的节点,若节点的值在G
中,则产生一个组件,每当遇到节点值不在G
中的节点,则当前组件结束。注意对链表结尾的处理。
class Solution:
def numComponents(self, head, G):
dic = set(G)
ans = 0
flag = 0
while head:
if head.val not in dic:
if flag == 1:
ans += 1
flag = 0
else:
flag = 1
head = head.next
else:
if flag == 1:
ans += 1
return ans
安排工作以达到最大收益
问题描述
有一些工作:difficulty[i] 表示第i个工作的难度,profit[i]表示第i个工作的收益。
现在我们有一些工人。worker[i]是第i个工人的能力,即该工人只能完成难度小于等于worker[i]的工作。
每一个工人都最多只能安排一个工作,但是一个工作可以完成多次。
举个例子,如果3个工人都尝试完成一份报酬为1的同样工作,那么总收益为 $3。如果一个工人不能完成任何工作,他的收益为 $0 。
我们能得到的最大收益是多少?
示例:
输入: difficulty = [2,4,6,8,10], profit = [10,20,30,40,50], worker = [4,5,6,7]
输出: 100
解释: 工人被分配的工作难度是 [4,4,6,6] ,分别获得 [20,20,30,30] 的收益。
提示:
-
1 <= difficulty.length = profit.length <= 10000
-
1 <= worker.length <= 10000
-
difficulty[i], profit[i], worker[i] 的范围是 [1, 10^5]
解法
先按工作难度将difficulty
和profit
排序,再将worker
排序。赚的最多的收益是在其能力之内的
收益最大值。能力高的工人不会比能力低的工人赚的少。注意,工作难度高不代表收益一定高。
class Solution:
def maxProfitAssignment(self, difficulty, profit, worker):
ans = 0
worker.sort()
ls = [[difficulty[i], profit[i]] for i in range(len(profit))]
ls.sort(key=lambda x: x[0])
loc = 0
flag = ls[0][1]
leng = len(ls)
for i in worker:
while loc < leng:
if i < ls[loc][0] and loc == 0:
break
elif i < ls[loc][0]:
ans += flag
break
else:
if flag < ls[loc][1]:
flag = ls[loc][1]
loc += 1
else:
ans += flag
return ans
翻转图像
题目描述
给定一个二进制矩阵 A
,我们想先水平翻转图像,然后反转图像并返回结果。
水平翻转图片就是将图片的每一行都进行翻转,即逆序。例如,水平翻转 [1, 1, 0]
的结果是 [0, 1, 1]
。
反转图片的意思是图片中的 0
全部被 1
替换, 1
全部被 0
替换。例如,反转 [0, 1, 1]
的结果是 [1, 0, 0]
。
示例 1
输入: [[1,1,0],[1,0,1],[0,0,0]]
输出: [[1,0,0],[0,1,0],[1,1,1]]
解释: 首先翻转每一行: [[0,1,1],[1,0,1],[0,0,0]];
然后反转图片: [[1,0,0],[0,1,0],[1,1,1]]
示例 2
输入: [[1,1,0,0],[1,0,0,1],[0,1,1,1],[1,0,1,0]]
输出: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
解释: 首先翻转每一行: [[0,0,1,1],[1,0,0,1],[1,1,1,0],[0,1,0,1]];
然后反转图片: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
说明
-
1 <= A.length = A[0].length <= 20
-
0 <= A[i][j] <= 1
解法
第一种解法
思路
实际上,对数组进行翻转并取反,数组只需要对比对称位上的数组是否相同,如果相同,就进行取反,如果不同,则不去取反,以为当对称位数字不同,进行一次翻转后在取反和原先的数字是相同的。
算法
var flipAndInvertImage = function(A) {
const len = A.length;
for (let k = 0; k < len; k++ ) {
let j = len - 1;
for(let i = 0; i <= ~~( j / 2 ); i++){
if ( A[k][i] === A[k][j - i] ) {
A[k][i] === 1 ? A[k][i] = A[k][j - i] = 0 : A[k][i] = A[k][j - i] = 1;
}
}
}
return A;
}
复杂度分析
暂无
第二种解法
思路
正常的进行交换取反值。这个解法中双非~~和 按位异或 ^ 的用法非常棒。
算法
var flipAndInvertImage2 = function(A) {
for (let i = 0; i < A.length; ++i) {
let last = A[i].length - 1;
for (let j = 0; j <= ~~(last / 2); ++j) {
[ A[i][j], A[i][last - j] ] = [ A[i][last - j] ^ 1, A[i][j] ^ 1 ];
}
}
return A;
};
复杂度分析
暂无
字母移位
问题描述
有一个由小写字母组成的字符串 S
,和一个整数数组 shifts
。
我们将字母表中的下一个字母称为原字母的 移位(由于字母表是环绕的, 'z'
将会变成 'a'
)。
例如·,shift('a') = 'b'
, shift('t') = 'u'
,, 以及 shift('z') = 'a'
。
对于每个 shifts[i] = x
, 我们会将 S
中的前 i+1
个字母移位 x
次。
返回将所有这些移位都应用到 S
后最终得到的字符串。
示例:
输入:S = "abc", shifts = [3,5,9]
输出:"rpl"
解释:
我们以 "abc" 开始。
将 S 中的第 1 个字母移位 3 次后,我们得到 "dbc"。
再将 S 中的前 2 个字母移位 5 次后,我们得到 "igc"。
最后将 S 中的这 3 个字母移位 9 次后,我们得到答案 "rpl"。
提示:
-
1 <= S.length = shifts.length <= 20000
-
0 <= shifts[i] <= 10 ^ 9
解法
对于S
中的每个字母,先将需要移动的长度求出,然后直接移位即可。从后往前遍历会比从前往后遍历快一点点,因为从前往后需要先求出shifts
的和。
class Solution:
def shiftingLetters(self, S, shifts):
mov = 0
ans = list(S)
for i in range(len(S) - 1, -1, -1):
mov += shifts[i]
ans[i] = chr((ord(S[i]) - 97 + mov % 26) % 26 + 97)
return ''.join(ans)
车队
问题描述
N
辆车沿着一条车道驶向位于 target
英里之外的共同目的地。
每辆车 i
以恒定的速度 speed[i]
(英里/小时),从初始位置 position[i]
(英里) 沿车道驶向目的地。
一辆车永远不会超过前面的另一辆车,但它可以追上去,并与前车以相同的速度紧接着行驶。
此时,我们会忽略这两辆车之间的距离,也就是说,它们被假定处于相同的位置。
车队 是一些由行驶在相同位置、具有相同速度的车组成的非空集合。注意,一辆车也可以是一个车队。
即便一辆车在目的地才赶上了一个车队,它们仍然会被视作是同一个车队。
会有多少车队到达目的地?
示例:
输入:target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3]
输出:3
解释:
从 10 和 8 开始的车会组成一个车队,它们在 12 处相遇。
从 0 处开始的车无法追上其它车,所以它自己就是一个车队。
从 5 和 3 开始的车会组成一个车队,它们在 6 处相遇。
请注意,在到达目的地之前没有其它车会遇到这些车队,所以答案是 3。
提示:
-
0 <= N <= 10 ^ 4
-
0 < target <= 10 ^ 6
-
0 < speed[i] <= 10 ^ 6
-
0 <= position[i] < target
-
所有车的初始位置各不相同。
解法
按起始位置倒序排序。第一辆车是第一个车队的开始。其后每辆车会加入它前面的车队,如果它到达终点的时间不大于它前面的车队,否则从它会形成一个以自己为首的新车队。车队到达终点的时间会被车队中的第一辆车的速度限制住。
class Solution:
def carFleet(self, target, position, speed):
car = [(pos, spe) for pos, spe in zip(position, speed)]
car.sort(reverse=True)
time = [(target - pos) / spe for pos, spe in car]
ls = []
for i in time:
if not ls:
ls.append(i)
else:
if i > ls[-1]:
ls.append(i)
return len(ls)
转置矩阵
题目描述
给定一个矩阵 A
, 返回 A
的转置矩阵。
矩阵的转置是指将矩阵的主对角线翻转,交换矩阵的行索引与列索引。
示例 1
输入:[[1,2,3],[4,5,6],[7,8,9]]
输出:[[1,4,7],[2,5,8],[3,6,9]]
示例 2
输入:[[1,2,3],[4,5,6]]
输出:[[1,4],[2,5],[3,6]]
提示
-
1 <= A.length <= 1000
-
1 <= A[0].length <= 1000
解题思路
思路
尺寸为 R x C
的矩阵 A
转置后会得到尺寸为 C x R
的矩阵 ans
,对此有 ans[c][r] = A[r][c]
。
让我们初始化一个新的矩阵 ans
来表示答案。然后,我们将酌情复制矩阵的每个条目。
算法
var transpose = function (A) {
if ( A.length === 1 && A[0].length === 1 ) return A;
let tran_matrix = [];
for ( let i = 0; i < A[0].length; ++i ) {
tran_matrix[i] = [];
for ( let j = 0; j < A.length; ++j ) {
tran_matrix[i][j] = A[j][i];
}
}
return tran_matrix;
};
复杂度分析
-
时间复杂度:O(R∗C),其中 R 和 C 是给定矩阵
A
的行数和列数。 -
空间复杂度:O(R∗C),也就是答案所使用的空间。
链表的中间结点
题目描述
给定一个带有头结点 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2:
输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 (序列化形式:[4,5,6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
提示:
- 给定链表的结点数介于 1 和 100 之间。
解法
快指针 fast 和慢指针 slow 初始指向 head。循环开始,fast 每次走两步,slow 每次走一步,当快指针为 null 或者 快指针下一个结点为 null(简单抠一下边界可以了),退出循环。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode middleNode(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode fast = head;
ListNode slow = head;
while (fast.next != null) {
// 快指针每次循环走两步,慢指针走一步
fast = fast.next.next;
slow = slow.next;
if (fast == null || fast.next == null) {
return slow;
}
}
return null;
}
}
救生艇
问题描述
第 i
个人的体重为 people[i]
,每艘船可以承载的最大重量为 limit
。
每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit
。
返回载到每一个人所需的最小船数。(保证每个人都能被船载)。
示例1:
输入:people = [1,2], limit = 3
输出:1
解释:1 艘船载 (1, 2)
示例2:
输入:people = [3,2,2,1], limit = 3
输出:3
解释:3 艘船分别载 (1, 2), (2) 和 (3)
示例3:
输入:people = [3,5,3,4], limit = 5
输出:4
解释:4 艘船分别载 (3), (3), (4), (5)
提示:
-
1 <= people.length <= 50000
-
1 <= people[i] <= limit <= 30000
解法
最重的人必定和最轻的人一组。如果最重的人加最轻的人的体重都超标了,则最重的人只能单独一组。使用头尾双指针即可解决问题。
class Solution:
def numRescueBoats(self, people, limit):
people.sort()
left = 0
right = len(people) - 1
ans = 0
while left < right:
if people[left] + people[right] <= limit:
ans += 1
left += 1
right -= 1
else:
ans += 1
right -= 1
else:
if left == right:
ans += 1
return ans
鸡蛋掉落
问题描述
你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑。
每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。
你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。
每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。
你的目标是确切地知道 F 的值是多少。
无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?
示例 1:
输入:K = 1, N = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,我们肯定知道 F = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,我们肯定知道 F = 1 。
如果它没碎,那么我们肯定知道 F = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 F 是多少。
示例 2:
输入:K = 2, N = 6
输出:3
示例 3:
输入:K = 3, N = 14
输出:4
思路:
问题最后求解最小移动次数, 即至少需要扔几次鸡蛋就能确定出扔下鸡蛋而鸡蛋不会摔破的最高楼层;
眨眼一看采用二分查找就是最快能找出答案的方法, 但是这个问题场景是使用有限个鸡蛋, 并且鸡蛋
可能会破, 所以需要采用稍微复杂点的三分查找才可以;
这里我们转变下思路, 求K个鸡蛋移动M步能测出的最大层数是多少, 当测出的层数大于所给楼层N时,
所移动的步数M的值即为所求!
采用DP问题的解决方式, 构造一个二维数组, 横向表示鸡蛋数(K), 纵向则是移动的步数(M), 然后
有一个公式: DP[K][M] = DP[K][M - 1] + (DP[K - 1][M - 1] + 1), 对它的解释就是当前一次
移动所测的楼层数等于上一次移动所测的楼层数加上这一次移动即将测出的楼层数(需要好好理解下);
根据前序和后序遍历构造二叉树
题目描述
返回与给定的前序和后序遍历匹配的任何二叉树。
pre
和 post
遍历中的值是不同的正整数。
示例 :
输入:pre = [1,2,4,5,3,6,7], post = [4,5,2,6,7,3,1]
输出:[1,2,3,4,5,6,7]
提示:
-
1 <= pre.length == post.length <= 30
-
pre[] 和 post[] 都是 1, 2, ..., pre.length 的排列
-
每个输入保证至少有一个答案。如果有多个答案,可以返回其中一个。
解法
由前序可以知道哪个是根节点(第一个节点即为根节点),由后序可以知道根节点的子节点有哪些(根节点之前的节点都是其子节点),递归可求。
class Solution:
def constructFromPrePost(self, pre, post):
if pre:
root = TreeNode(pre[0])
if len(pre) == 1:
return root
else:
for i in range(0, len(pre) - 1):
if post[i] == pre[1]:
root.left = self.constructFromPrePost(
pre[1:i + 1 + 1], post[:i + 1])
root.right = self.constructFromPrePost(
pre[i + 1 + 1:], post[i + 1:-1])
break
return root
else:
return None
Monotonic Array
An array is monotonic if it is either monotone increasing or monotone decreasing.
An array A is monotone increasing if for all i <= j, A[i] <= A[j]. An array A is monotone decreasing if for all i <= j, A[i] >= A[j].
Return true if and only if the given array A is monotonic.
Example 1:
Input: [1,2,2,3]
Output: true
Example 2:
Input: [6,5,4,4]
Output: true
Example 3:
Input: [1,3,2]
Output: false
Example 4:
Input: [1,2,4,5]
Output: true
Example 5:
Input: [1,1,1]
Output: true
按奇偶排序数组
题目描述
给定一个非负整数数组 A
,返回一个由 A
的所有偶数元素组成的数组,后面跟 A
的所有奇数元素。
你可以返回满足此条件的任何数组作为答案。
示例
输入:[3,1,2,4]
输出:[2,4,3,1]
输出 [4,2,3,1],[2,4,1,3] 和 [4,2,1,3] 也会被接受。
提示
-
1 <= A.length <= 5000
-
0 <= A[i] <= 5000
解法
官方题解缺失
第一种解法
思路
将数组A
中的奇数和偶数都分别的提取到一个数组中,最后进行合并数组操作。
算法
var sortArrayByParity = function (A) {
const len = A.length;
if (len === 1) return A;
let evenNumber = [], oddNumber = [];
for (let i = 0; i < len; i++) {
if (A[i] % 2 === 0) {
evenNumber.push(A[i]);
} else {
oddNumber.push(A[i]);
}
}
return evenNumber.concat(oddNumber);
};
复杂度分析
暂无
第二种解法
思路
利用数组实现一个特殊的队列,队列两头都可以进,前面进偶数,后面进奇数。
算法
var sortArrayByParity = function (A) {
const len = A.length;
if (len === 1) return A;
let eoNum = [];
A.forEach((item, index, array) => {
if (item % 2 === 1) {
eoNum.push(item);
} else {
eoNum.unshift(item);
}
});
return eoNum;
};
复杂度分析
暂无
第三种解法
思路
利用双指针i
、j
,i
指向数组的开始,j
指向数组的末尾,当i
<j
时,比较A[i]
和A[j]
,若i
指向的为奇数,j
指向的为偶数,交换A[i]
和A[j]
,i
加一,j
减一。若A[i]
为偶数,i
加一,不进行交换。若A[j]
为奇数,j
减一,不进行交换。
算法
// 第一种思路
var sortArrayByParity = function (A) {
const len = A.length;
if (len === 1) return A;
let i = 0, j = len - 1;
while (i < j) {
if ((A[i] % 2 === 1) && (A[j] % 2 === 0)) {
let temp = A[j];
A[j--] = A[i];
A[i++] = temp;
}
if (A[i] % 2 === 0) i++;
if (A[j] % 2 === 1) j--;
}
return A;
};
// 第二种思路
var sortArrayByParity = function (A) {
const len = A.length;
if (len === 1) return A;
let i = 0, j = len - 1;
while (i < j) {
while ( A[i] % 2 === 0 ) i++;
while ( A[j] % 2 === 1 ) j--;
while( i < j && (A[i] % 2 === 1) && (A[j] % 2 === 0) ){
let temp = A[j];
A[j--] = A[i];
A[i++] = temp;
}
}
return A;
};
复杂度分析
暂无
分割数组
问题描述
给定一个数组 A,将其划分为两个不相交(没有公共元素)的连续子数组 left 和 right, 使得:
-
left 中的每个元素都小于或等于 right 中的每个元素。
-
left 和 right 都是非空的。
-
left 要尽可能小。
在完成这样的分组后返回 left 的长度。可以保证存在这样的划分方法。
示例1:
输入:[5,0,3,8,6]
输出:3
解释:left = [5,0,3],right = [8,6]
示例2:
输入:[1,1,1,0,6,12]
输出:4
解释:left = [1,1,1,0],right = [6,12]
提示:
-
2 <= A.length <= 30000
-
0 <= A[i] <= 10^6
-
可以保证至少有一种方法能够按题目所描述的那样对 A 进行划分。
解法
从左到右遍历数组,维持三个标志,即left的结束位置loc
、left中最大的值vmx
、数组的第0
位与访问位置之间最大的值mx
。每次访问一个位置,若其值大于mx
,则应将其值赋予mx
,若其值小于vmx
,则应将其位置赋予loc
、将mx
赋予vmx
。
class Solution:
def partitionDisjoint(self, A):
loc = 0
vmx = A[0]
mx = A[0]
for i, el in enumerate(A):
if el > mx:
mx = el
if el < vmx:
loc = i
vmx = mx
return loc + 1
按奇偶排序数组 II
题目描述
给定一个非负整数数组 A
, A 中一半整数是奇数,一半整数是偶数。
对数组进行排序,以便当 A[i]
为奇数时,i
也是奇数;当 A[i]
为偶数时, i
也是偶数。
你可以返回任何满足上述条件的数组作为答案。
示例
输入:[4,2,5,7]
输出:[4,5,2,7]
解释:[4,7,2,5],[2,5,4,7],[2,7,4,5] 也会被接受。
提示
-
2 <= A.length <= 20000
-
A.length % 2 == 0
-
0 <= A[i] <= 1000
解题思路
思路
两个指针i
、j
分别指向偶数位和奇数位,当A[i]
是奇数时,寻找最近的A[j]
是偶数的位置,交换两个数字。直至遍历完整个数组的偶数位。
算法
var sortArrayByParityII = function(A) {
let index = A.length - 1, i = 0, j = 1; // index A的索引,i偶数位,j奇数位。
for( ; i < index; i += 2 ){
if( (A[i] & 1) != 0 ){ // 寻找A[i]是奇数的情况。
while( (A[j] & 1) != 0 ){ // 寻找A[j]是偶数的情况。
j += 2;
}
let temp = A[j];
A[j] = A[i];
A[i] = temp;
}
}
return A;
};
复杂度分析
暂无
二叉搜索树的范围和
问题描述
给定二叉搜索树的根结点 root,返回 L 和 R(含)之间的所有结点的值的和。
二叉搜索树保证具有唯一的值。
示例1:
输入:root = [10,5,15,3,7,null,18], L = 7, R = 15
输出:32
示例2:
输入:root = [10,5,15,3,7,13,18,1,null,6], L = 6, R = 10
输出:23
提示:
-
树中的结点数量最多为 10000 个。
-
最终的答案保证小于 2^31。
解法
需要返回二叉搜索树中所有大于等于 L 且小于等于 R 的节点值的和,则对每个节点进行遍历即可。
-
若节点的值大于等于 L 且小于等于 R,则加入统计中,并继续搜索其子节点。
-
因为是二叉搜索树,所以小于 L 的只需要搜索其右子节点,大于 R 的只需要搜索其左子节点。
-
等于的情况可以提出来单独处理,也可以不单独处理,单独处理只是会减少一些无用的迭代。
节点值等于 L 只需搜索右子节点,等于 R 只需搜索左子节点。
class Solution:
def rangeSumBST(self, root, L, R):
def searchBST(node):
if not node:
return
if L <= node.val <= R:
self.ans += node.val
searchBST(node.right)
searchBST(node.left)
elif node.val < L:
searchBST(node.right)
elif node.val > R:
searchBST(node.left)
self.ans = 0
searchBST(root)
return self.ans
按公因数计算最大组件大小
问题描述
给定一个由不同正整数的组成的非空数组 A
,考虑下面的图:
-
有
A.length
个节点,按从A[0]
到A[A.length - 1]
标记; -
只有当
A[i]
和A[j]
共用一个大于 1 的公因数时,A[i]
和A[j]
之间才有一条边。
返回图中最大连通组件的大小。
示例1:
输入:[4,6,15,35]
输出:4
示例2:
输入: [20,50,9,63]
输出: 2
示例3
输入: [2,3,6,7,4,12,21,39]
输出: 8
提示:
-
1 <= A.length <= 20000
-
1 <= A[i] <= 100000
解法
Naive 版本
这道题涉及到画连线,应当涉及到 union-find。初步解法是:
-
使用数组,初始化各节点的 root 为自身,并且维护各节点 root 所连通的图的节点数量(size)为 1
-
遍历数组中的每一个数,如果和其他数有大于 1 的公因数(也就是不互质),则用 union 方法将他们连在一起
-
在 union 的过程中,由于 union 的对象为各节点的根,因此需要使用 find 方法,并且缩短所涉及的节点和其根(root)的搜索距离,即将该节点与 root 直接连在一起。同时更新 size 数组的对应值
-
在遍历结束后,遍历 size 数组,找到 size 最大的。
class Solution {
public int largestComponentSize(int[] A) {
int n = A.length;
int[] root = new int[n];
int[] size = new int[n];
// 初始化 root 和 size array
for (int i = 0; i < n; i++) {
root[i] = i;
size[i] = 1;
}
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (!isCoprime(A[i], A[j])) {
union(size, root, i, j);
}
}
}
int max = 0;
for (int i = 0; i < n; i++) {
max = Math.max(size[i], max);
}
return max;
}
public void union(int[] size, int[] root, int i, int j) {
int rootI = find(root, i);
int rootJ = find(root, j);
if (rootI == rootJ) {
// 它们已经属于同一个 root
return;
}
// 决定两个节点如何连接和 size 的更新
if (size[rootI] > size[rootJ]) {
root[rootJ] = rootI;
size[rootI] += size[rootJ];
} else {
root[rootI] = rootJ;
size[rootJ] += size[rootI];
}
}
public int find(int[] root, int i) {
// 当某节点的根不是他自己时,则需要继续找到其 root
List<Integer> records = new LinkedList<>();
while (root[i] != i) {
records.add(i);
i = root[i];
}
// 将这些节点均指向其 root
for (Integer record: records) {
root[record] = i;
}
return i;
}
public boolean isCoprime(int x, int y) {
// 检查 x,y 是否互质
if (x == 1 || y == 1) {
return true;
}
while (true) {
int temp = x % y;
if (temp == 0) {
if (y == 1) {
return true;
}
return false;
}
x = y;
y = temp;
}
}
}
但是这个代码其实会超时,因为中间的遍历逻辑会耗费很长的时间,时间复杂度为 O(n²)。因此我们需要更快一点的解法。
优化版本
由于连通节点的条件是两个节点有公因数,那么他们可以通过这个公因数连在一起,而这个公因数又可以被分解为质因数,这样,我们只需要知道一个节点的质因数有哪些,并且将这些质因数和该节点相连。则对于每一个节点,我们都连接的是其质因数,或者说是质因数所对应的节点,但是本质上我们把这些有相同质因数的节点都连在了一起。具体步骤为:
-
维护 prime set,找到 100000 以内所有质数(找质数的方法应该都会吧)
-
维护三个数组,分别为:
-
各节点所连接的 root 编号,初始化为节点本身的编号
-
各节点为 root 时,连通图的 size,初始化为 1
-
各质数所连接到的节点对应的 root 的编号,初始化为 -1(因为开始时这些质数都没有和节点连在一起)
-
-
遍历节点,其中遍历所有质数,如果节点可以整除质数,则将该质数所连通的节点(如果有的话)和当前节点连在一起;并且更新该质数连通 到 新的连通图的 root 的编号。同时更新 root 对应的 size
-
遍历 size 数组,找到值最大的集合
而题中给定了节点值大小小于 100000,因此我们只需要找到 100000 里面的所有质数,并遍历节点将其连接到可以整除该节点的质数上,就等于是完成了有公因数之间的节点的连通。而根据我们上面的推算,遍历每个节点的所有质数时间复杂度是确定的为 O(np),p 为 100000 以内质数数量,即为 O(n),而 union-find 方法的每一个步骤 amortized 复杂度为 O(log*n),一个远小于 log n 的值。因此,我们通过优化了寻找连通边的方法,来达到优化算法的目的。
class Solution {
public int largestComponentSize(int[] A) {
int n = A.length, num = 100000 + 1, max = 0;
Set<Integer> primes = findPrime(num);
int[] root = new int[n];
int[] size = new int[n];
int[] primeToNode = new int[num];
// 一开始 prime 没有和数组 A 中的 node 连在一起
Arrays.fill(primeToNode, -1);
// 初始化 root 和 size array
for (int i = 0; i < n; i++) {
root[i] = i;
size[i] = 1;
}
for (int i = 0; i < n; i++) {
int curr = A[i];
// find all of its prime factors
for (Integer prime: primes) {
if (primes.contains(curr)) {
// 如果 curr 本身就是质数,则比较简便。
prime = curr;
}
if (curr % prime == 0) {
// 我们为 curr 找到一个质因数,则需要将该节点加入该 prime 已经连接到的根节点上
if (primeToNode[prime] != -1) {
// 该 prime 已经与数组 A 中 node 相连
union(size, root, primeToNode[prime], i);
}
primeToNode[prime] = find(root, i);
while (curr % prime == 0) {
// 将质因数 prime 全部剔除
curr = curr / prime;
}
}
if (curr == 1) {
break;
}
}
}
for (int i = 0; i < n; i++) {
max = Math.max(size[i], max);
}
return max;
}
public Set<Integer> findPrime(int num) {
boolean[] isPrime = new boolean[num];
Arrays.fill(isPrime, true);
Set<Integer> primes = new HashSet<>();
for (int i = 2; i < isPrime.length; i++) {
if (isPrime[i]) {
primes.add(i);
for (int j = 0; i * j < isPrime.length; j++) {
isPrime[i * j] = false;
}
}
}
return primes;
}
public void union(int[] size, int[] root, int i, int j) {
int rootI = find(root, i);
int rootJ = find(root, j);
if (rootI == rootJ) {
// 它们已经属于同一个 root
return;
}
if (size[rootI] > size[rootJ]) {
root[rootJ] = rootI;
size[rootI] += size[rootJ];
} else {
root[rootI] = rootJ;
size[rootJ] += size[rootI];
}
}
public int find(int[] root, int i) {
// 当某节点的根不是他自己时,则需要继续找到其 root
List<Integer> records = new LinkedList<>();
while (root[i] != i) {
records.add(i);
i = root[i];
}
// 将这些节点均指向其 root
for (Integer record: records) {
root[record] = i;
}
return i;
}
}
删列造序 II
问题描述
给定由 N
个小写字母字符串组成的数组 A
,其中每个字符串长度相等。
选取一个删除索引序列,对于 A
中的每个字符串,删除对应每个索引处的字符。
比如,有 A = ["abcdef", "uvwxyz"]
,删除索引序列 {0, 2, 3}
,删除后 A
为["bef", "vyz"]
。
假设,我们选择了一组删除索引 D
,那么在执行删除操作之后,最终得到的数组的元素是按 字典序(A[0] <= A[1] <= A[2] ... <= A[A.length - 1]
)排列的,然后请你返回 D.length
的最小可能值。
示例1:
输入:["ca","bb","ac"]
输出:1
解释:
删除第一列后,A = ["a", "b", "c"]。
现在 A 中元素是按字典排列的 (即,A[0] <= A[1] <= A[2])。
我们至少需要进行 1 次删除,因为最初 A 不是按字典序排列的,所以答案是 1。
示例2:
输入:["xc","yb","za"]
输出:0
解释:
A 的列已经是按字典序排列了,所以我们不需要删除任何东西。
注意 A 的行不需要按字典序排列。
也就是说,A[0][0] <= A[0][1] <= ... 不一定成立。
示例3
输入:["zyx","wvu","tsr"]
输出:3
解释:
我们必须删掉每一列。
提示:
-
1 <= A.length <= 100
-
1 <= A[i].length <= 100
解法
基本解法
原题中考虑的是对于某一列,我们是不是应该删掉它,那么反过来则可以考虑为:对于某一列,我们保留的条件是什么。
-
如果某列 i 可以被保留,那么我们只要保证
A[1][i] <= A[2][i] <= ... <= A[n][i] <= A[n + 1][i]
-
相反地,如果列 i 不能被保留,那么则需要让
A[1][i:] <= A[2][i:] <= ... <= A[n][i:] <= A[n + 1][i:]
其中 i: 表示第 i 列之后的所有字母(不包括 i)
显然,我们需要尽量地保留 i 以做到让我们的删列工作尽可能地简便。因此,这道题需要使用贪心算法。这需要我们在遍历输入的过程中,考虑新增的每列是否可以让原有已经存在的字符串们保留字典排序。因此,我们可以得到以下的算法:
class Solution {
public int minDeletionSize(String[] A) {
if (A == null || A.length <= 1) {
return 0;
}
int len = A.length, wordLen = A[0].length(), res = 0;
// 初始化空字符串数组,记录可以保留的每列
String[] curr = new String[len];
for (int j = 0; j < wordLen; j++) {
// 对于当前遍历到的列,需要决定其是否可以被保留
// 使用复制数组来决定当前列是否保留
String[] temp = Arrays.copyOf(curr, len);
for (int i = 0; i < len; i++) {
temp[i] += A[i].charAt(j);
}
if (!isSorted(temp)) {
// 如果未 sorted 则该列需要被删除
res += 1;
continue;
}
curr = temp;
}
return res;
}
public boolean isSorted(String[] temp) {
// 判断现有的 string 数组是否已经 sorted
for (int i = 0; i < temp.length - 1; i++) {
if (temp[i].compareTo(temp[i + 1]) > 0) {
return false;
}
}
return true;
}
}
该解法的时间复杂度为 O(NM²),其中 N 为 word 长度,M 为数组长度。空间复杂度为 O(NW),算法中包含两个复制出来的字符串数组。
优化解法
上述解法需要我们在 isSorted 方法中一直需要遍历现存元素的字符串长度来做到判定该数组是否已经 sorted,有没有更优化的方法让我们可以避免重复的遍历呢?思考下面的情况,对于数组
A = ["ar", "ax", "be", "bf", "bg"]
我们发现在我们决定可以保留第一个字符之后,后面要保证其 sorted 的条件可以简化为:
A[0] <= A[1] and A[2] <= A[3] <= A[4]
因此,当我们发现有严格 sorted 的字符后,可以设置一个 cut 的标志,表示 cut 两边的字符无需进行比较,并且这个 cut 是一劳永逸的,即当你发现这个 cut 的标志时,后面的遍历就再也不用比较这两个字符串了。因此,我们有以下的优化解法:
class Solution {
public int minDeletionSize(String[] A) {
if (A == null || A.length <= 1) {
return 0;
}
int len = A.length, wordLen = A[0].length(), res = 0;
boolean[] cut = new boolean[len];
search: for (int j = 0; j < wordLen; j++) {
// 判断第 j 列是否应当保留
for (int i = 0; i < len - 1; i++) {
if (!cut[i] && A[i].charAt(j) > A[i + 1].charAt(j)) {
res += 1;
continue search;
}
}
// 更新 cut 的信息
for (int i = 0; i < len - 1; i++) {
if (A[i].charAt(j) < A[i + 1].charAt(j)) {
cut[i] = true;
}
}
}
return res;
}
}
解法只用到了额外的 cut boolean 数组,空间复杂度为 O(M),时间复杂度为 O(NM),其中 N 为 word 长度,M 为数组长度。
重复 N 次的元素
问题描述
在大小为 2N
的数组 A
中有 N+1
个不同的元素,其中有一个元素重复了 N
次。
返回重复了 N
次的那个元素。
示例1:
输入:[1,2,3,3]
输出:3
示例2:
输入:[2,1,2,5,3,2]
输出:2
示例3:
输入:[5,1,5,2,5,3,5,4]
输出:5
提示:
-
4 <= A.length <= 10000
-
0 <= A[i] < 10000
-
A.length
为偶数
解法
-
直接用哈希表或者集合等数据结构就可解决。
-
由题意,只有一个元素是重复的,找到就行了。
class Solution:
def repeatedNTimes(self, A):
if A[0] == A[1] or A[0] == A[2] or A[0] == A[3]:
return A[0]
elif A[1] == A[2] or A[1] == A[3]:
return A[1]
elif A[2] == A[3]:
return A[2]
i = 4
while i < len(A):
if A[i] == A[i + 1]:
return A[i]
i += 2
有序数组的平方
题目描述
给定一个按非递减顺序排序的整数数组 A,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。
示例 1:
输入:[-4,-1,0,3,10]
输出:[0,1,9,16,100]
示例 2:
输入:[-7,-3,2,3,11]
输出:[4,9,9,49,121]
提示:
-
1 <= A.length <= 10000
-
-10000 <= A[i] <= 10000
-
A 已按非递减顺序排序。
解法
先平方后排序。
class Solution {
public int[] sortedSquares(int[] A) {
for (int i = 0, n = A.length; i < n; ++i) {
A[i] = A[i] * A[i];
}
Arrays.sort(A);
return A;
}
}
查询后的偶数和
题目描述
给出一个整数数组 A
和一个查询数组 queries
。
对于第 i
次查询,有 val = queries[i][0], index = queries[i][1]
,我们会把 val
加到 A[index]
上。然后,第 i
次查询的答案是 A
中偶数值的和。
(此处给定的 index = queries[i][1] 是从 0 开始的索引,每次查询都会永久修改数组 A。)
返回所有查询的答案。你的答案应当以数组 answer
给出,answer[i]
为第 i
次查询的答案。
示例
输入:A = [1,2,3,4], queries = [[1,0],[-3,1],[-4,0],[2,3]]
输出:[8,6,2,4]
解释:
开始时,数组为 [1,2,3,4]。
将 1 加到 A[0] 上之后,数组为 [2,2,3,4],偶数值之和为 2 + 2 + 4 = 8。
将 -3 加到 A[1] 上之后,数组为 [2,-1,3,4],偶数值之和为 2 + 4 = 6。
将 -4 加到 A[0] 上之后,数组为 [-2,-1,3,4],偶数值之和为 -2 + 4 = 2。
将 2 加到 A[3] 上之后,数组为 [-2,-1,3,6],偶数值之和为 -2 + 6 = 4。
提示
-
1 <= A.length <= 10000
-
-10000 <= A[i] <= 10000
-
1 <= queries.length <= 10000
-
-10000 <= queries[i][0] <= 10000
-
0 <= queries[i][1] < A.length
解法
第一种解法:调整数组和
思路
让我们尝试不断调整 S
,即每一步操作之后整个数组的偶数和。
我们操作数组中的某一个元素 A[index]
的时候,数组 A
其他位置的元素都应保持不变。如果 A[index]
是偶数,我们就从 S
中减去它,然后计算 A[index] + val
对 S
的影响(如果是偶数则在 S
中加上它)。
这里有一些例子:
-
如果当前情况为
A = [2,2,2,2,2]
、S = 10
,并且需要执行A[0] += 4
操作:我们应该先令S -= 2
,然后令S += 6
。最后得到A = [6,2,2,2,2]
与S = 14
。 -
如果当前情况为
A = [1,2,2,2,2]
、S = 8
,同时需要执行A[0] += 3
操作:我们会跳过第一次更新S
的步骤(因为A[0]
是奇数),然后令S += 4
。 最后得到A = [4,2,2,2,2]
与S = 12
。 -
如果当前情况为
A = [2,2,2,2,2]
、S = 10
,同时需要执行A[0] += 1
操作:我们先令S -= 2
,然后跳过第二次更新S
的操作(因为A[0] + 1
是奇数)。最后得到A = [3,2,2,2,2]
与S = 8
。 -
如果当前情况为
A = [1,2,2,2,2]
、S = 8
,同时需要执行A[0] += 2
操作:我们跳过第一次更新S
的操作(因为A[0]
是奇数),然后再跳过第二次更新S
的操作(因为A[0] + 2
是奇数)。最后得到A = [3,2,2,2,2]
与S = 8
。
这些例子充分展现了我们的算法在每一次询问操作之后应该如何调整 S
。
算法
var sumEvenAfterQueries = function (A, queries) {
const len = A.length; // A数组的长度
const qlen = queries.length; // queries数组的长度
let answer = [];
let S = 0;
for ( let i = 0; i < len; i++ ) {
if (A[i] % 2 == 0) {
S += A[i];
}
}
for ( let j = 0; j < qlen; j++ ) {
let val = queries[j][0];
let index = queries[j][1];
if ( A[index] % 2 == 0 ) {
S -= A[index];
}
A[index] += val;
if ( A[index] % 2 == 0 ) {
S += A[index]
}
answer.push(S);
}
return answer;
};
复杂度分析
-
时间复杂度:O(N+Q),其中 N 是数组
A
的长度, Q是询问queries
的数量。 -
空间复杂度:O(N+Q),事实上我们只使用了 O(1) 的额外空间。
先序遍历构造二叉树
问题描述
返回与给定先序遍历 preorder 相匹配的二叉搜索树(binary search tree)的根结点。
示例1:
输入:[8,5,1,7,10,12]
输出:[8,5,10,1,7,null,12]
提示:
-
1 <= preorder.length <= 100
-
The values of
preorder
are distinct.
解法
二叉树类的题目可以考虑使用递归中的分治法,让本次递归的根节点(sub-root)来管理自身子树的生成方式。而本题使用的是前序遍历法所生成的数组,则先检查了根节点,再检查左子树,再检查右子树。因此每层递归我们需要确定的是:
-
本层递归的根节点是什么?
-
根节点确定后,本层递归之后的左子树范围是什么,右子树的范围是什么?
对于第一个问题,我们知道前序遍历法的根节点一定是当前范围内的第一个元素;而对于第二个问题,我们知道右子树开始于第一个比当前根节点大的元素,而左子树结束于该元素的前面一个元素。在解决了这两个问题后,答案已经比较明确了,在每一层递归中,我们需要一个 start 和一个 end 来表示当前的递归所涉及的元素范围:
-
确定当前的递归是否结束(start > end || start >= end)
-
确定当前递归层的根节点(start)
-
确定左子树的范围(start + 1, leftEnd - 1)和右子树的范围(leftEnd, end)
因此有如下的递归解法:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode bstFromPreorder(int[] preorder) {
if (preorder == null || preorder.length == 0) {
return null;
}
// 进入分治法的递归
return helper(preorder, 0, preorder.length - 1);
}
private TreeNode helper(int[] preorder, int start, int end) {
// System.out.println("start: " + start + " end: " + end);
// 确认递归结束的标志,当 start == end 时,表示该区间只剩下一个 subRoot 节点
if (start > end) {
return null;
}
if (start == end) {
return new TreeNode(preorder[start]);
}
// 前序遍历,首先遍历到的为根
TreeNode root = new TreeNode(preorder[start]);
int leftEnd = start;
while (leftEnd <= end) {
if (preorder[leftEnd] > preorder[start]) {
break;
}
leftEnd++;
}
// System.out.println("leftEnd:" + leftEnd + " num: " + preorder[leftEnd]);
root.left = helper(preorder, start + 1, leftEnd - 1);
root.right = helper(preorder, leftEnd, end);
return root;
}
}
十进制的反码
问题描述
每个非负整数 N
都有其二进制表示。例如, 5
可以被表示为二进制 "101"
,11
可以用二进制 "1011"
表示,依此类推。注意,除 N = 0
外,任何二进制表示中都不含前导零。
二进制的反码表示是将每个 1
改为 0
且每个 0
变为 1
。例如,二进制数 "101"
的二进制反码为 "010"
。
给定十进制数 N,返回其二进制表示的反码所对应的十进制整数。
示例1:
输入:5
输出:2
解释:5 的二进制表示为 "101",其二进制反码为 "010",也就是十进制中的 2 。
示例2:
输入:7
输出:0
解释:7 的二进制表示为 "111",其二进制反码为 "000",也就是十进制中的 0 。
示例3:
输入:10
输出:5
解释:10 的二进制表示为 "1010",其二进制反码为 "0101",也就是十进制中的 5 。
提示:
0 <= N < 10^9
解法
求余数,取反(0 -> 1
, 1 -> 0
),累加结果。
注意 N = 0
的特殊情况。
class Solution {
public int bitwiseComplement(int N) {
if (N == 0) return 1;
int res = 0;
int exp = 0;
while (N != 0) {
int bit = N % 2 == 0 ? 1 : 0;
res += Math.pow(2, exp) * bit;
++exp;
N >>= 1;
}
return res;
}
}
1114.按顺序打印
问题描述
我们提供了一个类:
public class Foo {
public void one() { print("one"); }
public void two() { print("two"); }
public void three() { print("three"); }
}
三个不同的线程将会共用一个 Foo
实例。
-
线程 A 将会调用
one()
方法 -
线程 B 将会调用
two()
方法 -
线程 C 将会调用
three()
方法
请设计修改程序,以确保 two()
方法在 one()
方法之后被执行,three()
方法在 two()
方法之后被执行。
示例 1:
输入: [1,2,3]
输出: "onetwothree"
解释:
有三个线程会被异步启动。
输入 [1,2,3] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 two() 方法,线程 C 将会调用 three() 方法。
正确的输出是 "onetwothree"。
示例 2:
输入: [1,3,2]
输出: "onetwothree"
解释:
输入 [1,3,2] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 three() 方法,线程 C 将会调用 two() 方法。
正确的输出是 "onetwothree"。
解法
多线程同步问题,很好理解。
- C++ 解法
class Foo { //C++ 11
public:
Foo() {
_mutex1.lock();
_mutex2.lock();
}
void first(function<void()> printFirst) {
printFirst();
_mutex1.unlock();
}
void second(function<void()> printSecond) {
_mutex1.lock();
printSecond();
_mutex1.unlock();
_mutex2.unlock();
}
void third(function<void()> printThird) {
_mutex2.lock();
printThird();
_mutex2.unlock();
}
private:
std::mutex _mutex1;
std::mutex _mutex2;
};
- Java 解法
import java.util.concurrent.Semaphore;
class Foo {
private Semaphore twoS = new Semaphore(0);
private Semaphore threeS = new Semaphore(0);
public Foo() {
}
public void first(Runnable printFirst) throws InterruptedException {
printFirst.run();
twoS.release();
}
public void second(Runnable printSecond) throws InterruptedException {
twoS.acquire();
printSecond.run();
threeS.release();
}
public void third(Runnable printThird) throws InterruptedException {
threeS.acquire();
printThird.run();
}
}
1115.交替打印FooBar
问题描述
我们提供一个类:
class FooBar {
public void foo() {
for (int i = 0; i < n; i++) {
print("foo");
}
}
public void bar() {
for (int i = 0; i < n; i++) {
print("bar");
}
}
}
两个不同的线程将会共用一个 FooBar
实例。其中一个线程将会调用 foo()
方法,另一个线程将会调用 bar()
方法。
请设计修改程序,以确保 "foobar" 被输出 n 次。
示例 1:
输入: n = 1
输出: "foobar"
解释: 这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,"foobar" 将被输出一次。
示例 2:
输入: n = 2
输出: "foobarfoobar"
解释: "foobar" 将被输出两次。
思路
两锁交替的同步,后进行的代码先加锁原则
class FooBar {
private:
int n;
std::mutex _mutex1;
std::mutex _mutex2;
public:
FooBar(int n) {
this->n = n;
_mutex2.lock();
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
_mutex1.lock();
printFoo();
_mutex2.unlock();
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
_mutex2.lock();
printBar();
_mutex1.unlock();
}
}
};
1116.Print Zero Even Odd(leetcode未翻译)
问题描述
Suppose you are given the following code:
class ZeroEvenOdd {
public ZeroEvenOdd(int n) { ... } // constructor
public void zero(printNumber) { ... } // only output 0's
public void even(printNumber) { ... } // only output even numbers
public void odd(printNumber) { ... } // only output odd numbers
}
The same instance of ZeroEvenOdd
will be passed to three different threads:
-
Thread A will call
zero()
which should only output 0's. -
Thread B will call
even()
which should only ouput even numbers. -
Thread C will call
odd()
which should only output odd numbers.
Each of the thread is given a printNumber
method to output an integer. Modify the given program to output the series 010203040506
... where the length of the series must be 2n.
Example 1:
Input: n = 2
Output: "0102"
Explanation: There are three threads being fired asynchronously. One of them calls zero(), the other calls even(), and the last one calls odd(). "0102" is the correct output.
Example 2:
Input: n = 5
Output: "0102030405"
思路
一个同步问题,一个互斥问题,奇偶打印是互斥,0和奇/偶的打印是同步问题,所以用到3把锁
class ZeroEvenOdd {
private:
int n;
int flag;
mutex m1,m2,m3;
public:
ZeroEvenOdd(int n) {
this->n = n;
flag = 1; //奇偶判断
m1.lock();
m2.lock();
m3.lock();
}
// printNumber(x) outputs "x", where x is an integer.
void zero(function<void(int)> printNumber) {
m3.unlock();
for(int i = 0; i < n ;i++){
m3.lock();
printNumber(0);
if(flag == 1)flag = 0,m2.unlock();
else flag = 1,m1.unlock();
}
}
void odd(function<void(int)> printNumber) { //输出奇数
for(int i = 1;i <= n; i+=2){
m2.lock();
printNumber(i);
m3.unlock();
}
}
void even(function<void(int)> printNumber) { //输出偶数
for(int i = 2;i <= n; i+=2){
m1.lock();
printNumber(i);
m3.unlock();
}
}
};
1117. H2O 生成
问题描述
现在有两种线程,氢 oxygen
和氧 hydrogen
,你的目标是组织这两种线程来产生水分子。
存在一个屏障(barrier)使得每个线程必须等候直到一个完整水分子能够被产生出来。
氢和氧线程会被分别给予 releaseHydrogen
和 releaseOxygen
方法来允许它们突破屏障。
这些线程应该三三成组突破屏障并能立即组合产生一个水分子。
你必须保证产生一个水分子所需线程的结合必须发生在下一个水分子产生之前。
换句话说:
-
如果一个氧线程到达屏障时没有氢线程到达,它必须等候直到两个氢线程到达。
-
如果一个氢线程到达屏障时没有其它线程到达,它必须等候直到一个氧线程和另一个氢线程到达。
书写满足这些限制条件的氢、氧线程同步代码。
示例 1:
输入: "HOH"
输出: "HHO"
解释: "HOH" 和 "OHH" 依然都是有效解。
示例 2:
输入: "OOHHHH"
输出: "HHOHHO"
解释: "HOHHHO", "OHHHHO", "HHOHOH", "HOHHOH", "OHHHOH", "HHOOHH", "HOHOHH" 和 "OHHOHH" 依然都是有效解。
限制条件:
-
输入字符串的总长将会是 $3n, 1 ≤ n ≤ 50$;
-
输入字符串中的 “H” 总数将会是 $2n$;
-
输入字符串中的 “O” 总数将会是 $n$。
思路
同步问题,这里的策略是先输出2个H,再输出1个O
class H2O {
private:
int n_h;
mutex m_h,m_o;
public:
H2O() {
m_o.lock();
n_h = 2;
}
void hydrogen(function<void()> releaseHydrogen) {
m_h.lock();
releaseHydrogen();
n_h--;
if(n_h > 0)m_h.unlock();
else m_o.unlock();
}
void oxygen(function<void()> releaseOxygen) {
m_o.lock();
releaseOxygen();
n_h = 2;
m_h.unlock();
}
};
Day of the Year
Given a string date representing a Gregorian calendar date formatted as YYYY-MM-DD, return the day number of the year.
Example 1:
Input: date = "2019-01-09"
Output: 9
Explanation: Given date is the 9th day of the year in 2019.
Example 2:
Input: date = "2019-02-10"
Output: 41
Example 3:
Input: date = "2003-03-01"
Output: 60
Example 4:
Input: date = "2004-03-01"
Output: 61
Constraints:
-
date.length == 10
-
date[4] == date[7] == '-', and all other date[i]'s are digits
-
date represents a calendar date between Jan 1st, 1900 and Dec 31, 2019.
1287. 有序数组中出现次数超过 25% 的元素
题目描述
给你一个非递减的 有序 整数数组,已知这个数组中恰好有一个整数,它的出现次数超过数组元素总数的 25%。
请你找到并返回这个整数。
代码实现
- JavaScript
const findSpecialInteger = function(arr) {
let count = 0;
let item = -1;
for (var i = 0; i < arr.length; i++) {
if (item == arr[i]) {
count++;
} else {
item = arr[i];
count = 1;
}
if (count > arr.length * 0.25) {
return item;
}
}
return item;
};
- Java
class Solution {
public int findSpecialInteger(int[] arr) {
int total = arr.length;
for (int i = 0; i < total; ++i) {
if (arr[i] == arr[i + (total >> 2)]) {
return arr[i];
}
}
return 0;
}
}
- Python
class Solution:
def findSpecialInteger(self, arr: List[int]) -> int:
total = len(arr)
for i, val in enumerate(arr):
if val == arr[i + (total >> 2)]:
return val
return 0