LeetCode 刷题记录

LeetCode 练题记录

190. 颠倒二进制位

tag: 位运算

// Java 可以使用 Integer.reverse(n) 实现,底层为 分治法
public static int reverse(int i) {
// HD, Figure 7-1
// 01010101010101010101010101010101 -- 0x55555555
/**
* (i & 0x55555555) 得到 偶数位(从左往右 1 2)的二进制数
* << 1 左移到奇数位
* (i >>> 1) & 0x55555555 先将奇数位右移到偶数位,再得到 偶数位(从左往右 1 2)的二进制数
*/
// 每 2 位 交换
i = (i & 0x55555555) << 1 | (i >>> 1) & 0x55555555;
// 00110011001100110011001100110011 -- 0x33333333
// 每 4 位 交换
i = (i & 0x33333333) << 2 | (i >>> 2) & 0x33333333;
// 00001111000011110000111100001111 -- 0x0f0f0f0f
// 每 8 位 交换
i = (i & 0x0f0f0f0f) << 4 | (i >>> 4) & 0x0f0f0f0f;
// 11111111000000001111111100000000 -- 0xff00
/**
* 将 32 位 分为 4 份,每份 8 位 -- 为方便 假设为 1 2 3 4
* (i << 24) ------------- 4 0 0 0
* ((i & 0xff00) << 8) --- 0 3 0 0
* ((i >>> 8) & 0xff00) -- 0 0 2 0
* (i >>> 24) ------------ 0 0 0 1
* | 或运算 得到结果
*/
i = (i << 24) | ((i & 0xff00) << 8) | ((i >>> 8) & 0xff00) | (i >>> 24);
return i;
}
// 官方题解
class Solution {
// you need treat n as an unsigned value
public int reverseBits(int n) {
int rev = 0;
for (int i = 0; i < 32 && n != 0; ++i) {
/**
* n & 1 -- 按位与 相同为 1,相异为 0
* << -- 左移 被移除的最高位丢弃,空缺位补 0
* |= -- 或等于 先进行或运算,再赋值
* -----------
* (n & 1) 得到最后一位字符
* << (31 - i) 左移 (31 - i) 位
* |= 添加到 rev
*/
rev |= (n & 1) << (31 - i);
/**
* >>>= 无符号右移 + 赋值 -- 被移位二进制最高位无论是 0 或 1,空缺位都用 0 补
* -----------
* 无符号右移一位
*/
n >>>= 1;
}
return rev;
}
}
// 官方题解
class Solution {
private static final int M1 = 0x55555555; // 01010101010101010101010101010101
private static final int M2 = 0x33333333; // 00110011001100110011001100110011
private static final int M4 = 0x0f0f0f0f; // 00001111000011110000111100001111
private static final int M8 = 0x00ff00ff; // 00000000111111110000000011111111
public int reverseBits(int n) {
n = n >>> 1 & M1 | (n & M1) << 1;
n = n >>> 2 & M2 | (n & M2) << 2;
n = n >>> 4 & M4 | (n & M4) << 4;
n = n >>> 8 & M8 | (n & M8) << 8;
return n >>> 16 | n << 16;
}
}

279. 完全平方数

tag: 递归

class Solution {
// 记录已经计算过的数值
private Map<Integer, Integer> map = new HashMap<>();
// 递归
public int numSquares(int n) {
if (n <= 1)
return n;
if (map.containsKey(n))
return map.get(n);
int num = (int) Math.sqrt(n);
// 取最小值
int sum = 0, sumMin = Integer.MAX_VALUE;
for (int i = num; i >= 1; i--) {
sum = 1 + numSquares(n - i * i);
sumMin = sum < sumMin ? sum : sumMin;
}
map.put(n, sumMin); // 保存最小值
return sumMin;
}
}

171. Excel 表列序号

tag: Excel

class Solution {
public int titleToNumber(String columnTitle) {
char[] title = columnTitle.toCharArray(); // 将字符串转换为字符数组
int sum = 0; // 结果
for (char ch : title) {
sum = sum * 26 + ch - 'A' + 1;
}
return sum;
}
}

168. Excel表列名称

tag: 字符串

class Solution {
public String convertToTitle(int columnNumber) {
StringBuilder builder = new StringBuilder(); // 存储字符串
// columnNumber-- 为何如此操作? ==> 因为 不是从 0 下标开始,计算时,需要先减 1,才能计算结果
while (columnNumber-- != 0) {
builder.append((char) ('A' + columnNumber % 26));
columnNumber /= 26;
}
// 逆置,并转化为字符串
return builder.reverse().toString();
}
}

125. 验证回文串

tag: 回文串、双指针

class Solution {
public boolean isPalindrome(String s) {
char[] chS = s.toLowerCase().toCharArray(); // 将字符串转化为小写字符,再转化为字符数组
int len = s.length(); // 数组长度
int i = 0, j = len - 1; // 左边界 & 右边界
while (i < j) {
// 不符合匹配条件的字符跳过
while (j >= 0 && !isCharAndNum(chS[j]))
j--;
while (i <= len - 1 && !isCharAndNum(chS[i]))
i++;
// 判断是否是回文
if (i < j && chS[i++] != chS[j--])
return false;
}
return true;
}
// 判断是否是匹配字符
private boolean isCharAndNum(char ch) {
if (ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9')
return true;
return false;
}
}

111. 二叉树的最小深度

tag: 深度优先搜索、二叉树

class Solution {
private int min_d = Integer.MAX_VALUE;
public int minDepth(TreeNode root) {
if (root == null)
return 0;
depth(root, 1);
return min_d;
}
private void depth(TreeNode root, int d) {
if (d > min_d)
return;
if (root.left == null && root.right == null) {
min_d = min_d > d ? d : min_d;
return;
}
if (root.left != null)
depth(root.left, d + 1);
if (root.right != null)
depth(root.right, d + 1);
}
}

69. x 的平方根

tag: 二分查找、牛顿迭代

class Solution {
public int mySqrt(int x) {
int l = 0, r = x, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if ((long) mid * mid <= x) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return ans;
}
}
// 作者:LeetCode-Solution
// 链接:https://leetcode-cn.com/problems/sqrtx/solution/x-de-ping-fang-gen-by-leetcode-solution/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
// 官方题解
class Solution {
public int mySqrt(int x) {
if (x == 0) {
return 0;
}
double C = x, x0 = x;
while (true) {
double xi = 0.5 * (x0 + C / x0);
if (Math.abs(x0 - xi) < 1e-7) {
break;
}
x0 = xi;
}
return (int) x0;
}
}

66. 加一

// 我的代码
class Solution {
public int[] plusOne(int[] digits) {
List<Integer> ans = new ArrayList<>();
int j = digits.length - 1, carr = 1;
while (j >= 0 || carr > 0) {
carr += j >= 0 ? digits[j--] : 0;
ans.add(carr % 10);
carr /= 10;
}
int len = ans.size();
int[] result = new int[len];
for (int i = 0; i < len; i++) {
result[i] = ans.get(len - 1 - i);
}
return result;
}
}
// 大佬的代码
class Solution {
public int[] plusOne(int[] digits) {
for (int i = digits.length - 1; i >= 0; i--) {
digits[i]++;
digits[i] = digits[i] % 10;
if (digits[i] != 0) return digits;
}
digits = new int[digits.length + 1];
digits[0] = 1;
return digits;
}
}
作者:yhhzw
链接:https://leetcode-cn.com/problems/plus-one/solution/java-shu-xue-jie-ti-by-yhhzw/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

406. 根据身高重建队列

tag: 二维数组、排序

class Solution {
public int[][] reconstructQueue(int[][] people) {
// 升序排序,对于高度相同的人,按照第二关键字降序排序,为何如此处理,看官方解析
// eg: 排序前:[[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] ==>
// 排序后:[[4,4],[5,2],[5,0],[6,1],[7,1],[7,0]]
Arrays.sort(people, (o1, o2) -> {
if (o1[0] - o2[0] == 0)
return o2[1] - o1[1];
return o1[0] - o2[0];
});
int n = people.length; // 人数
int[][] ans = new int[n][]; // 存储结果
// 按照第二关键字,寻找插入的位置
for (int[] person : people) {
int spaces = person[1] + 1;
for (int i = 0; i < n; ++i) {
if (ans[i] == null) {
--spaces;
if (spaces == 0) {
ans[i] = person;
break;
}
}
}
}
return ans;
}
}

28. 实现 strStr()

tag: 字符串匹配

/** 暴力匹配 */
class Solution {
public int strStr(String haystack, String needle) {
int lenH = haystack.length();
int lenN = needle.length();
// needle 为空串,按照题目要求,返回 0
if (lenN == 0)
return 0;
// 特判 -- needle 的长度 大于 haystack ,说明 haystack 不存在 needle ,字符
if (lenN > lenH)
return -1;
// 暴力寻找
for (int i = 0; i <= lenH - lenN; i++) {
int j = 0;
int k = i;
while (j < lenN) {
if (haystack.charAt(k++) != needle.charAt(j++))
break;
if (j == lenN)
return i;
}
}
return -1;
}
}

416. 分割等和子集

tag: 0-1 背包问题、动态规划

/** 官方题解 */
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
// 长度小于 2,无法划分为两个子集
if (n < 2) {
return false;
}
int sum = 0, maxNum = 0;
// 求总和、最大值
for (int num : nums) {
sum += num;
maxNum = Math.max(maxNum, num);
}
// 总和为奇数,无法划分为两个相等的子集:sum - target = target ==> sum 一定是偶数
if (sum % 2 != 0) {
return false;
}
int target = sum / 2;
// 最大值大于 target,说明其他元素之和一定小于 target
if (maxNum > target) {
return false;
}
/**
* 动态规划
* dp[i][j] -- 表示从数组的 [0~i] 下标范围内选取若干个正整数(可以是 0 个),是否存在一种选取方案使得被选取的正整数的和等于 j
*/
boolean[][] dp = new boolean[n][target + 1];
// 边界条件
for (int i = 0; i < n; i++) {
dp[i][0] = true;
}
// i == 0 时,只有一个正整数 nums[0] 可以被选取,因此 dp[0][nums[0]]=true
dp[0][nums[0]] = true;
for (int i = 1; i < n; i++) {
int num = nums[i];
for (int j = 1; j <= target; j++) {
/**
* j 大于或等于 num,
* 选择 num ==> dp[i - 1][j - num]
* 不选择 num ==> dp[i - 1][j]
*/
if (j >= num) {
dp[i][j] = dp[i - 1][j] | dp[i - 1][j - num];
} else {
// j 小于 num
dp[i][j] = dp[i - 1][j];
}
}
}
// 结果
return dp[n - 1][target];
}
}

438. 找到字符串中所有字母异位词

tag: 滑动窗口

// 滑动窗口
public List<Integer> findAnagrams(String s, String p) {
// 字符串 s 和 p 的长度
int sLen = s.length(), pLen = p.length();
// 特判 -- s 的长度小于 p 的长度
if (sLen < pLen) {
return new ArrayList<Integer>();
}
List<Integer> ans = new ArrayList<Integer>(); // 存放结果
int[] sCount = new int[26]; // 存储字符串 s 每个字符的个数
int[] pCount = new int[26]; // 存储字符串 p 每个字符的个数
// 统计字符串中每个字符的个数
for (int i = 0; i < pLen; ++i) {
++sCount[s.charAt(i) - 'a'];
++pCount[p.charAt(i) - 'a'];
}
// 特判 - 如果数组相同,说明是异位词
if (Arrays.equals(sCount, pCount)) {
ans.add(0);
}
// 动态维护数组 sCount 长度恒定为 pLen
for (int i = 0; i < sLen - pLen; ++i) {
// 整体右滑一个窗口
--sCount[s.charAt(i) - 'a'];
++sCount[s.charAt(i + pLen) - 'a'];
if (Arrays.equals(sCount, pCount)) {
ans.add(i + 1);
}
}
return ans;
}

437. 路径总和 III

tag: 二叉树、深度优先搜索

// 暴力深度优先算法
// 统计符合条件的个数
private int number;
public int pathSum(TreeNode root, int targetSum) {
// 特判
if (root == null)
return 0;
// 存放每个结点
Deque<TreeNode> queue = new LinkedList<>();
queue.add(root);
// 临时变量
TreeNode temp;
while (!queue.isEmpty()) {
// 当前结点出队
temp = queue.remove();
// 深度优先搜索
dfs(temp, 0, targetSum);
// 左结点不空,入队
if (temp.left != null)
queue.add(temp.left);
// 右结点不空,入队
if (temp.right != null)
queue.add(temp.right);
}
return number;
}
private void dfs(TreeNode root, int sum, int targetSum) {
// 当前结点值等于剩余目标值,个数加一
if (sum == targetSum) {
number++;
}
// 左结点不空,遍历左子树
if (root.left != null)
dfs(root.left, sum + root.val, targetSum);
// 右结点不空,遍历右子树
if (root.right != null)
dfs(root.right, sum - root.val, targetSum);
}

4. 寻找两个正序数组的中位数

解题思路:题目要求寻找两个有序数组的中位数,很直接明了的一个思路就是先将两个有序数组合并为一个有序数组,再求中位数medNum = (nums[len / 2] + nums[(len - 1) / 2]) / 2。时间复杂度为O(m+n),空间复杂度为O(m+n),不能达到题目要求的时间复杂度为O(log(m+n))。

class Solution
{
public:
double findMedianSortedArrays(vector<int> &nums1, vector<int> &nums2)
{
return mergeArrays(nums1, nums2);
}
// O(m + n)
double mergeArrays(vector<int> &nums1, vector<int> &nums2)
{
int len1 = nums1.size();
int len2 = nums2.size();
vector<int> nums3;
int i = 0, j = 0;
while (i < len1 && j < len2)
{
if (nums1[i] < nums2[j])
{
nums3.push_back(nums1[i++]);
}
else
{
nums3.push_back(nums2[j++]);
}
}
while (i < len1)
{
nums3.push_back(nums1[i++]);
}
while (j < len2)
{
nums3.push_back(nums2[j++]);
}
// med = (num[i / 2] + num[(i - 1) / 2]) / 2
int len3 = nums3.size();
return len3 > 0 ? (nums3[len3 / 2] + nums3[(len3 - 1) / 2]) / 2.0 : 0;
}
};

33. 搜索旋转排序数组

解题思路:题目要求在数组中找到目标值,给出的数组是有序并且旋转过的。可以直接遍历数组,但时间复杂度达不到题目要求O(logn)。思路是使用二分查找,关键点是二分之后要判断子数组是否有序。

class Solution {
// 二分查找
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 求数组的中间位置
if (nums[mid] == target) {
return mid;
}
if (nums[mid] >= nums[left]) { // 左半部分是有序的
if (target < nums[mid] && target >= nums[left]) {
right = mid - 1; // 目标值在左半部分
} else {
left = mid + 1;
}
} else {
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
}
}

34. 在排序数组中查找元素的第一个和最后一个位置

解题思路:题目要求在有序数组找出目标值的开始和结束位置。可以直接遍历整个数组求得,时间复杂度为 O(n),但题目要求时间复杂度为 O(log2n)。因为是有序,所以可以用二分查找求得。

class Solution {
public:
// 二分查找
vector<int> searchRange(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1; // 左边界、右边界
// 求左边界
while (left <= right) {
int mid = left + (right - left) / 2; // 求数组的中间位置
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 边界处理
int finLeft = (left < nums.size() && nums[left] == target) ? left : -1;
// 求右边界 -- 操作同求左边界
left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
int finRight = (right > -1 && nums[right] == target) ? right : -1;
return {finLeft, finRight};
}
};

49. 字母异位词分组

解题思路:题目要求将字母异位词进行分组,那么将异位词排序后,就会变成相同的单词。所以可以使用哈希表进行分组,先将单词进行排序,作为键,值存储原来的单词。

class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
vector<vector<string>> strV; // 存放所有分组字谜
unordered_map<string, vector<string>> mp;
// 重新对单词进行排序后,判断是否相同单词进行分组 -- 通过哈希表存储
for (auto&& str : strs) {
string key = str;
sort(key.begin(), key.end());
mp[key].push_back(str);
}
// 将分好组的单词存放到容器中
for (auto it = mp.begin(); it != mp.end(); it++) {
strV.push_back(it->second);
}
return strV;
}
};

53. 最大子数组和

76. 最小覆盖子串

滑动窗口:使用左右指针,记录左边界和右边界,右边界阔张,左边界收缩,得到最小覆盖子串。使用 HashMap 记录字符串中每个字符的个数

class Solution {
public:
unordered_map<char, int> ori, cnt;
string minWindow(string s, string t) {
// 统计每个字符的字数
for (const auto &c : t) {
++ori[c];
}
int l = 0, r = -1;
int len = INT_MAX, ansL = -1, check = t.length(); // check 检查是否已匹配所有字符
int s_len = s.length();
while (r < s_len) {
// 不是模式串中的字符
if (ori.find(s[++r]) == ori.end()) continue;
// 统计匹配到的字符字数
if (++cnt[s[r]] <= ori[s[r]]) {
check--;
}
// 检查是否已匹配所有字符
while (!check && l <= r) {
if (r - l + 1 < len) {
len = r - l + 1; // 满足条件的最短长度
ansL = l; // 记录左边界
}
// 左滑
if (ori.find(s[l]) != ori.end()) {
if (--cnt[s[l]] < ori[s[l]]) ++check;
}
++l;
}
}
return ansL == -1 ? string() : s.substr(ansL, len);
}
};

78. 子集

解题思路:用容器保存每一个子集,每次往容器中的每一个子集添加一个元素,并保留原有子集,直到添加完最后一个元素。也可用回溯法。

// 子集
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<int> subSet;
vector<vector<int>> subSets(1, subSet);
for (auto&& num : nums) {
vector<vector<int>> tmp = subSets;
for (auto&& sub : tmp) {
sub.push_back(num);
subSets.push_back(sub);
}
}
return subSets;
}
};

79. 单词搜索

解题思路:这题的思路和 78. 子集 一样都可以用回溯法,依次匹配每一个字符,如果不匹配,回溯到上一个字符,寻找其他路径。(看了一下官方代码。自己写的一塌糊涂,菜,很多细节都没有处理到位)用偏移量数组来记录方位,可以省去很多冗余代码

class Solution {
public:
vector<vector<int>> visited; // 判断是否已访问
int len = 0, m = 0, n = 0;
bool exist(vector<vector<char>>& board, string word) {
m = board.size();
n = board[0].size();
len = word.length();
vector<int> v(n, 0);
vector<vector<int>> vv(m, v);
visited = vv;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
visited[i][j] = 1; // 标记已访问
if (match(board, word, i, j, 0)) return true;
}
}
return false;
}
bool match(vector<vector<char>>& board, string& word, int i, int j, int t) {
if (board[i][j] != word[t]) { // 当前字符不匹配,回溯,并标记为未访问
visited[i][j] = 0;
return false;
}
if (t == len - 1) {
return true;
}
bool flag = false;
if (i - 1 >= 0 && !visited[i - 1][j]) { // 向上
visited[i - 1][j] = 1;
flag = match(board, word, i - 1, j, t + 1);
}
if (flag) return true;
if (j - 1 >= 0 && !visited[i][j - 1]) { // 向左
visited[i][j - 1] = 1;
flag = match(board, word, i, j - 1, t + 1);
}
if (flag) return true;
if (i + 1 < m && !visited[i + 1][j]) { // 向下
visited[i + 1][j] = 1;
flag = match(board, word, i + 1, j, t + 1);
}
if (flag) return true;
if (j + 1 < n && !visited[i][j + 1]) { // 向右
visited[i][j + 1] = 1;
flag = match(board, word, i, j + 1, t + 1);
}
visited[i][j] = 0; // 回溯,如果是没有匹配成功,标记为未访问
return flag;
}
};

96. 不同的二叉搜索树

解题思路:看到这题的时候就想到了卡特兰数,所以用了取巧的方法。针对整数越界,可以使用 double 或者 long long 类型。看了官方的题解,把代码贴上了,作为参考。

// 卡特兰数推导式 : C0 = 1 C(n + 1) = 2*(2n+1)/(n+2)Cn
// 卡特兰数 C(n 2n)/n+1 C(n 2n)
class Solution {
public:
int numTrees(int n) {
// 取巧的方式:卡特兰数 C(n 2n)/n+1 C(n 2n) : 组合数
double fnum = 1, mnum = 1;
for (int i = 2 * n; i > n; --i) {
fnum *= i;
mnum *= (i - n);
}
return (int)(fnum / mnum / (n + 1) + 0.5);
}
};
class Solution {
public:
int numTrees(int n) {
vector<int> G(n + 1, 0);
G[0] = 1;
G[1] = 1;
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= i; ++j) {
G[i] += G[j - 1] * G[i - j];
}
}
return G[n];
}
};
// 官方 -- 动态规划
class Solution {
public:
int numTrees(int n) {
vector<int> G(n + 1, 0);
G[0] = 1;
G[1] = 1;
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= i; ++j) {
G[i] += G[j - 1] * G[i - j];
}
}
return G[n];
}
};
/**
* 作者:LeetCode-Solution
* 链接:https://leetcode-cn.com/problems/unique-binary-search-trees/solution/bu-tong-de-er-cha-sou-suo-shu-by-leetcode-solution/
*/

105. 从前序与中序遍历序列构造二叉树

解题思路:二叉树的构造,根据前序(NLR:遍历根结点 左子树 右子树)和中序(LNR 左子树 根结点 右子树)遍历的特性,创建二叉树。在前序序列中找到根结点,再遍历中序序列划分左右子树。递归

class Solution {
public:
int len;
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
len = inorder.size();
return buildTreeByPreIn(preorder, inorder, 0, 0, len - 1);
}
TreeNode *buildTreeByPreIn(vector<int> &preoder, vector<int> &inoder, int p, int s, int e) {
if (s > e || p >= len) return nullptr;
TreeNode *root = nullptr;
for (int i = s; i <= e; ++i) {
if (inoder[i] == preoder[p]) { // 中序序列中找到根结点
root = new TreeNode(preoder[p]);
root->left = buildTreeByPreIn(preoder, inoder, p + 1, s, i - 1); // 划分左子树
root->right = buildTreeByPreIn(preoder, inoder, p + i - s + 1, i + 1, e); // 划分右子树
}
}
return root;
}
};

124. 二叉树中的最大路径和

解题思路:节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值,递归计算左右子节点的最大贡献值。

class Solution {
// 保存最大值,初始化
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return maxSum;
}
public int maxGain(TreeNode node) {
if (node == null) {
return 0;
}
// 递归计算左右子节点的最大贡献值
// 只有在最大贡献值大于 0 时,才会选取对应子节点
int leftGain = Math.max(maxGain(node.left), 0);
int rightGain = Math.max(maxGain(node.right), 0);
// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
int priceNewpath = node.val + leftGain + rightGain;
// 更新答案
maxSum = Math.max(maxSum, priceNewpath);
// 返回节点的最大贡献值
return node.val + Math.max(leftGain, rightGain);
}
}

148. 排序链表

解题思路:使用自底向上的归并排序算法

class Solution {
public ListNode sortList(ListNode head) {
// 特值
if (head == null) {
return head;
}
int length = 0;
ListNode node = head;
// 求链表长度 -- O(n)
while (node != null) {
length++;
node = node.next;
}
// 头结点
ListNode dummyHead = new ListNode(0, head);
// <<= 左移并赋值
for (int subLength = 1; subLength < length; subLength <<= 1) {
// prev -- 前驱结点,curr -- 当前结点
ListNode prev = dummyHead, curr = dummyHead.next;
while (curr != null) {
// 截取长度为 subLength 的链表
ListNode head1 = curr;
for (int i = 1; i < subLength && curr.next != null; i++) {
curr = curr.next;
}
ListNode head2 = curr.next;
curr.next = null;
curr = head2;
for (int i = 1; i < subLength && curr != null && curr.next != null; i++) {
curr = curr.next;
}
// 保存剩余链表
ListNode next = null;
if (curr != null) {
next = curr.next;
curr.next = null;
}
// 合并两个链表
ListNode merged = merge(head1, head2);
prev.next = merged;
while (prev.next != null) {
prev = prev.next;
}
curr = next;
}
}
return dummyHead.next;
}
// 合并两个链表
public ListNode merge(ListNode head1, ListNode head2) {
ListNode dummyHead = new ListNode(0);
ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
while (temp1 != null && temp2 != null) {
if (temp1.val <= temp2.val) {
temp.next = temp1;
temp1 = temp1.next;
} else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if (temp1 != null) {
temp.next = temp1;
} else if (temp2 != null) {
temp.next = temp2;
}
return dummyHead.next;
}
}

152. 乘积最大子数组

tag: 动态规划、滑动数组

53. 最大子序和

class Solution {
public int maxProduct(int[] nums) {
// 记录最大子数组和,最小子数组和,结果
int maxF = nums[0], minF = nums[0], ans = nums[0];
int length = nums.length; // 数组长度
for (int i = 1; i < length; ++i) {
int mx = maxF, mn = minF;
maxF = Math.max(mx * nums[i], Math.max(nums[i], mn * nums[i]));
minF = Math.min(mn * nums[i], Math.min(nums[i], mx * nums[i]));
ans = Math.max(maxF, ans);
}
return ans;
}
}

155. Min Stack

class MinStack {
private Deque<Integer> minStack; // 保存栈内的最小值
private Deque<Integer> stack; // 保存栈元素
public MinStack() {
// 初始化
stack = new LinkedList<Integer>();
minStack = new LinkedList<Integer>();
minStack.push(Integer.MAX_VALUE); // 保存整数的最大值
}
public void push(int val) {
stack.push(val);
minStack.push(Math.min(minStack.peek(), val)); // 存入最小值
}
public void pop() {
stack.pop();
minStack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}

198. 打家劫舍

tag: 动态规划、滚动数组

class Solution {
/**
* 动态规划 dp
* 边界条件:
* dp[0] = nums[0]
* dp[1] = max{nums[0], nums[1]}
*
* 状态转移方程:
* dp[i] = max{dp[i - 1], nums[i] + dp[i - 2]}
*
* 可使用滚动数组进行优化
*
* @param nums
* @return
*/
public int rob(int[] nums) {
int len = nums.length; // 数组长度
// 特判
if (len == 1)
return nums[0];
// 动态规划数组
// int[] dp = new int[len];
// 边界条件
// dp[0] = nums[0];
// dp[1] = Math.max(nums[0], nums[1]);
int dp1 = nums[0];
int dp2 = Math.max(nums[0], nums[1]);
for (int i = 2; i < len; ++i) {
// dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
int tmp = dp2;
dp2 = dp2 > (dp1 + nums[i]) ? dp2 : (dp1 + nums[i]);
dp1 = tmp;
}
return dp2;
}
}

200. 岛屿数量

tag: 深度优先、广度优先、并查集

解题思路:使用深度优先搜索 或 广度优先搜索 或 并查集

// 官方 并查集
class Solution {
class UnionFind {
int count; // 岛屿个数
int[] parent; // 父节点
int[] rank; // rank[i] 表示以 i 为根的集合所表示的树的层数 - 1
// 初始化 O(m * n)
public UnionFind(char[][] grid) {
count = 0;
int m = grid.length;
int n = grid[0].length;
parent = new int[m * n];
rank = new int[m * n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == '1') {
parent[i * n + j] = i * n + j;
++count;
}
rank[i * n + j] = 0;
}
}
}
// O(h)
public int find(int i) {
if (parent[i] != i)
parent[i] = find(parent[i]);
return parent[i];
}
// O(h)
public void union(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] > rank[rooty]) {
parent[rooty] = rootx;
} else if (rank[rootx] < rank[rooty]) {
parent[rootx] = rooty;
} else {
parent[rooty] = rootx;
rank[rootx] += 1;
}
--count;
}
}
public int getCount() {
return count;
}
}
// 查找岛屿的数量
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
int[][] directions = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };
UnionFind uf = new UnionFind(grid);
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
grid[r][c] = '0';
for (int[] dir : directions) {
int tmp_r = r + dir[0], tmp_c = c + dir[1];
if (tmp_r >= 0 && tmp_r < nr && tmp_c >= 0 && tmp_c < nc) {
uf.union(r * nc + c, tmp_r * nc + tmp_c);
}
}
// if (r - 1 >= 0 && grid[r - 1][c] == '1') {
// uf.union(r * nc + c, (r - 1) * nc + c);
// }
// if (r + 1 < nr && grid[r + 1][c] == '1') {
// uf.union(r * nc + c, (r + 1) * nc + c);
// }
// if (c - 1 >= 0 && grid[r][c - 1] == '1') {
// uf.union(r * nc + c, r * nc + c - 1);
// }
// if (c + 1 < nc && grid[r][c + 1] == '1') {
// uf.union(r * nc + c, r * nc + c + 1);
// }
}
}
}
return uf.getCount();
}
}

207. 课程表

tag: 深度优先搜索、广度优先搜索、拓扑排序

解题思路:根据题意可知,这是一道拓扑排序问题。判断图是否存在回路,如果存在,说明不是拓扑排序

class Solution {
private List<List<Integer>> edges; // 存储的关系图
private int[] visited; // 0 -- 未搜索,1 -- 搜索中,2 -- 已完成搜索
private boolean valid = true; // 是否可以学完课程 -- 可以学完:true
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 初始化
edges = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
edges.add(new ArrayList<>());
}
visited = new int[numCourses];
// 存储信息 先修课 --> 选修
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
// 搜索
for (int i = 0; i < numCourses && valid; i++) {
if (visited[i] == 0) {
DFS(i);
}
}
return valid;
}
// 深度优先搜索
public void DFS(int u) {
visited[u] = 1;
for (int v : edges.get(u)) {
if (visited[v] == 0) {
DFS(v);
// 如果 valid == false ,说明存在环,无法完成选修,直接返回
if (!valid) {
return;
}
} else if (visited[v] == 1) {
valid = false;
return;
}
}
visited[u] = 2;
}
}

208. 实现 Trie (前缀树)

tag: Trie、前缀树、字典树

/**
* 前缀树
*/
class Trie {
private Trie[] children; // 孩子结点
private boolean isEnd; // 是否为单词结尾
public Trie() {
// 初始化
children = new Trie[26]; // 0:表示 a,1:表示 b,依次类推
isEnd = false;
}
// 插入
public void insert(String word) {
Trie node = this; // 获得当前结点
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
int index = ch - 'a';
// 不存在当前子结点,新建
if (node.children[index] == null) {
node.children[index] = new Trie();
}
node = node.children[index];
}
// 单词结尾
node.isEnd = true;
}
// 单词是否符合
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
// 单词前缀是否符合
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null;
}
// 寻找单词前缀
private Trie searchPrefix(String prefix) {
Trie node = this;
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {
return null;
}
node = node.children[index];
}
return node;
}
}

221. 最大正方形

tag: 动态规划

/**
* 动态规划 -- 求最大正方形边长
*/
class Solution {
public int maximalSquare(char[][] matrix) {
int maxSide = 0;
// 特判
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return maxSide;
}
// 行数、列数
int rows = matrix.length, columns = matrix[0].length;
// 动态规划表
int[][] dp = new int[rows][columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (matrix[i][j] == '1') {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
// 最大正方形边长
maxSide = Math.max(maxSide, dp[i][j]);
}
}
}
int maxSquare = maxSide * maxSide;
return maxSquare;
}
}

236. 二叉树的最近公共祖先

tag: 递归

/**
* 递归
*/
class Solution {
/** 公共祖先 */
private TreeNode ans;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
dfs(root, p, q);
return ans;
}
// 通过深度优先搜索寻找最近公共祖先
private boolean dfs(TreeNode root, TreeNode p, TreeNode q) {
if (root == null)
return false;
boolean lson = dfs(root.left, p, q);
boolean rson = dfs(root.right, p, q);
if ((lson && rson) || (root.val == p.val || root.val == q.val) && (lson || rson))
ans = root;
return lson || rson || (root.val == p.val || root.val == q.val);
}
}

283. 移动零

/**
* 取巧方式:直接在后面补零
*/
class Solution {
public void moveZeroes(int[] nums) {
int len = nums.length, j = 0; // 数组长度,标记当前数组位置
for (int i = 0; i < len; i++) {
if (nums[i] != 0) {
nums[j++] = nums[i];
}
}
// 补零
while (j < len) {
nums[j++] = 0;
}
}
}

448. 找到所有数组中消失的数字

/**
* 根据题意,将数组值可以与数组下标 + 1 一一对应,
* 故交换数组中的两个数,使数组值与数组下标 + 1 对应,
* 找不到对应的则赋值为 -1。
*
* 注:官方的题解比较好,通俗易懂,我的操作复杂
*/
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
List<Integer> disNumList = new ArrayList<>();
int len = nums.length;
for (int i = 0; i < len; i++) {
// 将值与下标 + 1 一一对应
while (nums[i] != (i + 1) && nums[i] != -1) {
// 暂时找不到对应的值,赋值为 -1
if (nums[nums[i] - 1] == nums[i])
nums[i] = -1;
else
swap(nums, i, nums[i] - 1);
}
}
// 寻找值为 -1 的数据
for (int i = 0; i < len; i++) {
if (nums[i] == -1) {
disNumList.add(i + 1);
}
}
return disNumList;
}
/**
* 交换数组中的两个数
*
* @param nums
* @param i
* @param j
*/
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
/**
* 官方题解
*/
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
int n = nums.length;
for (int num : nums) {
int x = (num - 1) % n;
nums[x] += n;
}
List<Integer> ret = new ArrayList<Integer>();
for (int i = 0; i < n; i++) {
if (nums[i] <= n) {
ret.add(i + 1);
}
}
return ret;
}
}
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
int n = nums.length;
for (int num : nums) {
int x = (num - 1) % n;
nums[x] += n;
}
List<Integer> ret = new ArrayList<Integer>();
for (int i = 0; i < n; i++) {
if (nums[i] <= n) {
ret.add(i + 1);
}
}
return ret;
}
}

461. 汉明距离

tag: 位运算 ^

/**
* 按位异或(相异为 1,相同为 0),计算 1 的个数
*/
public int hammingDistance(int x, int y) {
int z = x ^ y;
int sum = 0;
while (z > 0) {
if (z % 2 == 1)
sum++;
z /= 2;
}
return sum;
}
/**
* 官方题解:内置位计数功能
* Integer.bitCount() 源码
* public static int bitCount(int i) {
// HD, Figure 5-2
i = i - ((i >>> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
i = (i + (i >>> 4)) & 0x0f0f0f0f;
i = i + (i >>> 8);
i = i + (i >>> 16);
return i & 0x3f;
}
*
*/
class Solution {
public int hammingDistance(int x, int y) {
return Integer.bitCount(x ^ y);
}
}
/**
* 官方题解:Brian Kernighan 算法
* 记 f(x) 表示 x 和 x-1 进行与运算所得的结果(即 f(x) = x & (x−1)),那么f(x) 恰为 x 删去其二进制表示中最右侧的1的结果。
*/
class Solution {
public int hammingDistance(int x, int y) {
int s = x ^ y, ret = 0;
while (s != 0) {
s &= s - 1;
ret++;
}
return ret;
}
}

494. 目标和

tag: 动态规划、滚动数组、背包问题

public class Question_494 {
// 官方题解
// nums 的总和为 sum,添加'-'号的元素个数和为 neg,则添加'+'号的元素的个数为 (sum - neg)
// 则有 (sum - neg) - neg = target ==> neg = (sum - target) / 2;
// 因此题目要求转化为 使元素和为 neg 的个数。使用动态规划解决
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
// 求 nums 的总和
for (int num : nums) {
sum += num;
}
int diff = sum - target;
// 如果 diff 小于 0 或者 无法被 2 整除,说明不符合条件
if (diff < 0 || diff % 2 != 0) {
return 0;
}
int n = nums.length, neg = diff / 2;
// 动态规划 -- dp[i][j] 表示前 i 个元素中,元素和为 j 的元素个数,dp[n][neg] 为答案
int[][] dp = new int[n + 1][neg + 1];
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
int num = nums[i - 1];
for (int j = 0; j <= neg; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= num) {
dp[i][j] += dp[i - 1][j - num];
}
}
}
return dp[n][neg];
}
// 官方题解
// 使用 滚动数组 进行优化
public int findTargetSumWays2(int[] nums, int target) {
int sum = 0;
for (int num : nums) {
sum += num;
}
int diff = sum - target;
if (diff < 0 || diff % 2 != 0) {
return 0;
}
int neg = diff / 2;
// dp 的每一行的计算只和上一行有关,因此可以使用滚动数组的方式
int[] dp = new int[neg + 1];
dp[0] = 1;
for (int num : nums) {
for (int j = neg; j >= num; j--) {
dp[j] += dp[j - num];
}
}
return dp[neg];
}
}

538. 把二叉搜索树转换为累加树

tag: 反中序遍历、二叉搜索树

Morris算法图解

// Morris 遍历
class Solution1 {
public TreeNode convertBST(TreeNode root) {
int sum = 0;
TreeNode node = root;
while (node != null) {
// 右子树为空,当前结点累加,开始遍历左子树
if (node.right == null) {
sum += node.val;
node.val = sum;
node = node.left;
} else {
// 右子树不为空,寻找右子树的最左结点(不为 node)
TreeNode succ = getSuccessor(node);
// 最左结点的左指针为空
if (succ.left == null) {
// 最左结点的左指针指向当前结点
/**
* 为何如此处理?
* 根据(反)中序遍历的特性,遍历右子树的最后一个结点是右子树的最左结点,
* 当处理到最左结点时,发现左指针不为空,说明已处理完成,直接累加,并开始遍历左子树。
* 即 将空闲的左指针指向了下个要遍历的结点,从而不使用递归就能找到下一个结点。
*/
succ.left = node;
// 遍历右子树
node = node.right;
} else {
// 不为空说明 succ.left 指向的是 node,说明已遍历完右子树,开始遍历左子树
succ.left = null;
sum += node.val;
node.val = sum;
node = node.left;
}
}
}
return root;
}
/**
* 寻找 node 结点的右子树的最左结点,并且该结点不为 node 结点
*
* @param node
* @return
*/
public TreeNode getSuccessor(TreeNode node) {
TreeNode succ = node.right;
while (succ.left != null && succ.left != node) {
succ = succ.left;
}
return succ;
}
}
// 超级傻的方式
class Solution {
public TreeNode convertBST(TreeNode root) {
Deque<TreeNode> stack = new LinkedList<>();
if (root == null)
return root;
TreeNode p = root, q = null;
// 中序遍历 -- 二叉搜索树 从右子树开始
while (p != null || !stack.isEmpty()) {
// p 结点不空,继续往右子树走,同时将当前结点进栈
if (p != null) {
stack.push(p);
p = p.right;
} else {
// p 结点为空,说明已经遍历完右子树,遍历根结点,再遍历左子树
p = stack.pop();
// 保存遍历的上一个结点,值相加
if (q != null) {
p.val += q.val;
}
q = p;
p = p.left;
}
}
return root;
}
}
// 官方题解 -- 反中序遍历 -- 递归
class Solution {
int sum = 0;
public TreeNode convertBST(TreeNode root) {
if (root != null) {
convertBST(root.right);
sum += root.val;
root.val = sum;
convertBST(root.left);
}
return root;
}
}

543. 二叉树的直径

tag: 深度优先搜索

class Solution {
private int maxDiameter;
public int diameterOfBinaryTree(TreeNode root) {
diameter(root);
return maxDiameter;
}
// 深度优先搜索
private int diameter(TreeNode root) {
if (root == null)
return 0;
int ldiameter = 0, rdiameter = 0;
// 左子树不空,求左子树的最大高度 + 1
if (root.left != null)
ldiameter = 1 + diameter(root.left);
// 右子树不空,求右子树的最大高度 +—1
if (root.right != null)
rdiameter = 1 + diameter(root.right);
// 判断经过 root 结点的直径(左子树的最大高度 + 1 + 右子树的最大高度 + 1)是否大于最大直径
maxDiameter = maxDiameter > (ldiameter + rdiameter) ? maxDiameter : (ldiameter + rdiameter);
// 返回 root 的最大高度
return ldiameter > rdiameter ? ldiameter : rdiameter;
}
}

617. 合并二叉树

tag: 深度优先搜索、广度优先搜索

/**
* 官方题解
*/
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1 == null) {
return root2;
}
if (root2 == null) {
return root1;
}
TreeNode merged = new TreeNode(root1.val + root2.val);
merged.left = mergeTrees(root1.left, root2.left);
merged.right = mergeTrees(root1.right, root2.right);
return merged;
}
}

739. 每日温度

tag: 单调栈

/**
* 使用单调栈
*/
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int len = temperatures.length; // 数组长度
// 特解
if (len == 1)
return new int[] { 0 };
int[] result = new int[len];
Deque<Integer> stack = new LinkedList<>(); // 单调栈
for (int i = 0; i < len; i++) {
// 判断当日温度 是否有高于 之前的温度
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int index = stack.pop();
result[index] = i - index; // 升温的时间
}
stack.push(i);
}
// 栈中剩余的是之后的日子没有高于它的温度
// 数组初始化为 0,因此此遍历可不要
while (!stack.isEmpty()) {
result[stack.pop()] = 0;
}
return result;
}
}

剑指 Offer 03. 数组中重复的数字

类似题目:448.找到所有数组中消失的数字

/**
* 方式一:
* 题目中说明:在一个长度为 n 的数组 nums 里的所有数字都在 【0~n-1】 的范围内,
* 因此可以遍历数组,并在遍历数组的时候将交换值,使得数组下标与值一一对应【eg: nums[1] = 1】。
* 如果在遍历的时候发现当前下标与值不对应,但值作为下标时,对应值已经相等【nums[nums[i]] ==
* nums[i]】,则说明是重复的数据,直接返回。
*/
class Solution {
public int findRepeatNumber(int[] nums) {
int len = nums.length; // 数组长度
for (int i = 0; i < len; i++) {
// 如果当前下标与值相等,将元素归位,使数组下标与值一一对应【eg: nums[1] = 1】
while (nums[i] != i) {
// 将值作为下标时,此下标的值已是一一对应的,说明是重复元素
if (nums[nums[i]] == nums[i])
return nums[i];
swap(nums, i, nums[i]);
}
}
return 0;
}
// 交换数组中的两个元素值
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
/**
* 方式二:
* 依旧是利用题目性质【长度为 n 的数组 nums 里的所有数字都在 【0~n-1】 的范围内】
* 遍历数组过程中,将值作为下标,对应值加 n,遍历时,如果存在已经加 n 的情况,说明是重复的元素
*/
class Solution2 {
public int findRepeatNumber(int[] nums) {
int len = nums.length;
for (int i = 0; i < len; i++) {
int j = nums[i] % len;
if (nums[j] >= len)
return j;
nums[j] += len;
}
return -1;
}
}
posted @   请叫大叔~  阅读(42)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示