算法,是每个研发都必须跨过的一道坎,不管面试还是学习中都非常重要。当然,很多业务研发日常工作中算法可能会用的比较少,但是,算法从来都不是应该按照使用的多少来决定其重要性。你可以去借鉴别人写好的算法代码,但是不能不明白算法的原理及目的,甚至应该去优化算法代码。就像我们念书时候,每个数学题都不可能只有一种解题思路,不管业务工作,还是算法工作,总会有很多的实现方式,作为研发同学,找到效率最高的实现方式能获得极大的成就感。而学习算法,就是帮助我们拓展思路,加强编程能力,提高实际解决问题能力的一种方式。
以下是整理的常用的算法合集,以供参考学习
- 两数之和:给定一个整数数组和一个目标值,在数组中找到两个数,使它们的和等于目标值。要求时间复杂度为O(n)。可以使用哈希表来解决这个问题,将数组中的每个元素作为哈希表的键,将它的索引作为哈希表的值.
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement)) {
return new int[] { map.get(complement), i };
}
map.put(nums[i], i);
}
throw new IllegalArgumentException("No two sum solution");
}
该算法使用哈希表,时间复杂度为 O(n)。首先创建一个空哈希表 map,然后遍历数组 nums 中的每个元素,对于每个元素,计算出目标值 target 减去该元素后的差值 complement,检查哈希表 map 中是否存在键为 complement 的值,如果存在,就返回结果,否则将该元素作为键,将其索引作为值存储在哈希表中。如果遍历结束后没有找到满足条件的结果,则抛出异常 "No two sum solution"。
- 反转链表:给定一个单链表,将其反转。可以使用迭代或递归的方式来实现,其中迭代的方式比较简单,递归的方式代码量较少。
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode current = head;
while (current != null) {
ListNode nextNode = current.next;
current.next = prev;
prev = current;
current = nextNode;
}
return prev;
}
其中,ListNode是链表节点的数据结构,包括一个整型val和一个指向下一个节点的指针next:
class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
以上代码中,我们使用三个指针prev、current和nextNode来迭代遍历链表并反转节点,具体步骤如下: 将当前节点的下一个节点保存到nextNode中,以便遍历下一个节点。 将当前节点的next指针指向前一个节点prev,实现链表反转。 将prev指针指向当前节点,以便链表向后遍历。 将current指针指向nextNode,以便遍历下一个节点。 最后返回prev指针,即为反转后的链表头节点。
- 快速排序:给定一个无序数组,使用快速排序算法对其进行排序。快速排序算法是一种分治算法,通过将数组分成两个部分,对每个部分分别进行排序,然后再将它们合并起来。
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high); // 先分区
quickSort(arr, low, pivotIndex - 1); // 递归排序左半部分
quickSort(arr, pivotIndex + 1, high); // 递归排序右半部分
}
}
private static int partition(int[] arr, int low, int high) {
int pivot = arr[low]; // 选第一个元素作为枢轴
while (low < high) {
while (low < high && arr[high] >= pivot) {
high--;
}
arr[low] = arr[high]; // 交换比枢轴小的元素到左侧
while (low < high && arr[low] <= pivot) {
low++;
}
arr[high] = arr[low]; // 交换比枢轴大的元素到右侧
}
arr[low] = pivot; // 将枢轴元素放到最终位置
return low;
}
快速排序的基本思想是选择一个元素作为枢轴(pivot),然后把数组分成两部分,左边部分的所有元素都小于枢轴,右边部分的所有元素都大于枢轴。分成两部分之后,分别对左右两部分进行递归排序,直到整个数组有序。在上面的实现中,选择了数组的第一个元素作为枢轴。
- 二叉树的遍历:给定一个二叉树,实现它的前序遍历、中序遍历和后序遍历。可以使用递归或栈来实现。
// 定义二叉树的节点
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
// 前序遍历
public void preorderTraversal(TreeNode root) {
if (root == null) {
return;
}
System.out.print(root.val + " ");
preorderTraversal(root.left);
preorderTraversal(root.right);
}
// 中序遍历
public void inorderTraversal(TreeNode root) {
if (root == null) {
return;
}
inorderTraversal(root.left);
System.out.print(root.val + " ");
inorderTraversal(root.right);
}
// 后序遍历
public void postorderTraversal(TreeNode root) {
if (root == null) {
return;
}
postorderTraversal(root.left);
postorderTraversal(root.right);
System.out.print(root.val + " ");
}
- 最长公共子序列:给定两个字符串,求它们的最长公共子序列。可以使用动态规划算法来解决这个问题,其中状态转移方程需要根据字符串的相等和不相等情况来确定。
public static String longestCommonSubsequence(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[][] dp = new int[m+1][n+1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (s1.charAt(i-1) == s2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
int i = m, j = n;
StringBuilder sb = new StringBuilder();
while (i > 0 && j > 0) {
if (s1.charAt(i-1) == s2.charAt(j-1)) {
sb.append(s1.charAt(i-1));
i--;
j--;
} else if (dp[i-1][j] > dp[i][j-1]) {
i--;
} else {
j--;
}
}
return sb.reverse().toString();
}
该算法使用二维数组dp来记录最长公共子序列的长度,其中dp[i][j]表示s1的前i个字符和s2的前j个字符的最长公共子序列的长度。如果s1.charAt(i-1) == s2.charAt(j-1),说明当前字符相等,可以将其加入到最长公共子序列中,长度加1;否则,当前字符不能包含在最长公共子序列中,需要在s1的前i-1个字符和s2的前j个字符的最长公共子序列和s1的前i个字符和s2的前j-1个字符的最长公共子序列中选取一个更大的值作为dp[i][j]。
在计算出dp数组之后,可以根据dp数组和输入字符串s1和s2来构造最长公共子序列。具体地,从dp[m][n]开始,不断往左上方移动,如果s1.charAt(i-1) == s2.charAt(j-1),说明当前字符包含在最长公共子序列中,将其加入到结果字符串中;否则,如果dp[i-1][j] > dp[i][j-1],说明最长公共子序列中不包含s1.charAt(i-1),需要往上移动;否则,说明最长公共子序列中不包含s2.charAt(j-1),需要往左移动。
- 最大子序和:给定一个整数数组,找到一个具有最大和的连续子数组,返回其最大和。可以使用动态规划算法来解决这个问题,其中状态转移方程需要根据当前元素和前一个元素的关系来确定。
可以使用动态规划来解决最大子序和问题,状态转移方程如下: 设 表示以第 个元素为结尾的连续子数组的最大和,则: 当 时,; 当 时,。 最终的最大子序和为 ,其中 是数组的长度。
以下是 Java 代码实现:
public int maxSubArray(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
int maxSum = dp[0];
for (int i = 1; i < n; i++) {
if (dp[i - 1] <= 0) {
dp[i] = nums[i];
} else {
dp[i] = dp[i - 1] + nums[i];
}
maxSum = Math.max(maxSum, dp[i]);
}
return maxSum;
}
该算法的时间复杂度为 ,空间复杂度也为 。
- 二分查找:给定一个有序数组和一个目标值,使用二分查找算法在数组中查找目标值。可以使用迭代或递归的方式来实现,其中迭代的方式比较简单,递归的方式代码量较少。
public static int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
在代码中,首先设置左右指针的位置,然后通过 while 循环不断缩小查找范围,直到找到目标值或查找范围为空。每次循环中,通过计算中间位置来确定查找范围,然后根据目标值与中间值的大小关系来移动左右指针。如果找到目标值,返回其索引;否则返回 -1 表示未找到。
- 字符串相加:给定两个字符串形式的非负整数,计算它们的和并以字符串形式返回。可以使用模拟加法的方式来实现,从字符串的末尾开始相加,注意进位和长度不一致的情况。
public String addStrings(String num1, String num2) {
int i = num1.length() - 1, j = num2.length() - 1, carry = 0;
StringBuilder sb = new StringBuilder();
while (i >= 0 || j >= 0 || carry != 0) {
int x = i >= 0 ? num1.charAt(i) - '0' : 0;
int y = j >= 0 ? num2.charAt(j) - '0' : 0;
int sum = x + y + carry;
sb.append(sum % 10);
carry = sum / 10;
i--;
j--;
}
return sb.reverse().toString();
}
该方法使用了StringBuilder来存储计算结果。首先定义了两个指针i和j,分别指向num1和num2的末尾。同时,定义了一个进位标志carry,初始值为0。接着进入while循环,对num1和num2从末尾开始进行相加,每次取出一位数字x和y,然后将它们与进位标志carry相加,得到sum。然后将sum对10取余,得到当前位的数字,将其追加到StringBuilder中。接着将sum除以10,得到进位标志,最后将i、j分别减1,继续循环直到两个字符串全部相加完毕。最后,将StringBuilder翻转并转换为字符串,即得到了字符串相加的结果。
- 有效的括号:给定一个只包括左括号、右括号和字符'{'、'}'、'('、')'的字符串,判断字符串是否有效。可以使用栈来解决这个问题,将左括号压入栈中,遇到右括号时弹出栈顶元素,并判断它们是否匹配。
import java.util.Stack;
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '{' || c == '[') {
stack.push(c);
} else {
if (stack.isEmpty()) {
return false;
}
char top = stack.pop();
if ((c == ')' && top != '(') || (c == '}' && top != '{') || (c == ']' && top != '[')) {
return false;
}
}
}
return stack.isEmpty();
}
}
该算法的时间复杂度为O(n),其中n为字符串的长度。算法使用了一个栈来保存左括号,并在遇到右括号时进行匹配。如果字符串中所有括号都能够匹配,则最后栈应该为空。
- 合并K个升序链表:给定K个升序排列的链表,将它们合并成一个升序排列的链表。可以使用分治算法来解决这个问题,将K个链表分成两个部分,分别对每个部分进行排序,然后将它们合并起来。
可以使用优先队列来实现合并K个升序链表的问题。将K个链表的头结点加入优先队列中,队列中的元素按照值的大小进行排序,每次取出队列中最小的元素,将它的下一个节点加入队列中,直到队列为空为止。这个过程相当于是将所有节点从小到大排序,然后重新构建成一个新的链表。
以下是Java代码实现示例:
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> queue = new PriorityQueue<>(new Comparator<ListNode>() {
public int compare(ListNode l1, ListNode l2) {
return l1.val - l2.val;
}
});
for (ListNode node : lists) {
if (node != null) {
queue.add(node);
}
}
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
while (!queue.isEmpty()) {
ListNode node = queue.poll();
tail.next = node;
tail = tail.next;
if (node.next != null) {
queue.add(node.next);
}
}
return dummy.next;
}
- 最长回文子串:给定一个字符串,找到它的最长回文子串,并返回该子串。可以使用中心扩展法或动态规划算法来解决这个问题,中心扩展法枚举回文串的中心位置,动态规划算法则需要根据子问题的结果来确定当前子问题的解。
动态规划算法可以解决这个问题,定义一个二维数组dp[i][j]表示子串s[i:j]是否是回文串,状态转移方程如下: 当i == j时,dp[i][j] = true; 当i < j时,如果s[i] == s[j]并且s[i+1:j-1]也是回文串,则dp[i][j] = true,否则dp[i][j] = false; 使用双指针遍历字符串,计算dp数组并记录最长回文子串的位置,然后根据位置返回结果即可。
以下是Java代码实现:
public String longestPalindrome(String s) {
int n = s.length();
boolean[][] dp = new boolean[n][n];
int start = 0, end = 0;
for (int j = 0; j < n; j++) {
for (int i = 0; i <= j; i++) {
if (s.charAt(i) == s.charAt(j) && (j - i <= 2 || dp[i+1][j-1])) {
dp[i][j] = true;
if (j - i > end - start) {
start = i;
end = j;
}
}
}
}
return s.substring(start, end + 1);
}
- 电话号码的字母组合:给定一个仅包含数字2-9的字符串,返回它所能表示的所有字母组合。可以使用回溯算法来解决这个问题,枚举每个数字对应的所有字母,逐步构建组合并回溯到上一个位置继续尝试其他可能的字母组合。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
private Map<Character, String> map = new HashMap<>();
public List<String> letterCombinations(String digits) {
List<String> res = new ArrayList<>();
if (digits == null || digits.length() == 0) {
return res;
}
map.put('2', "abc");
map.put('3', "def");
map.put('4', "ghi");
map.put('5', "jkl");
map.put('6', "mno");
map.put('7', "pqrs");
map.put('8', "tuv");
map.put('9', "wxyz");
StringBuilder sb = new StringBuilder();
backtrace(digits, 0, sb, res);
return res;
}
private void backtrace(String digits, int index, StringBuilder sb, List<String> res) {
if (index == digits.length()) {
res.add(sb.toString());
return;
}
String s = map.get(digits.charAt(index));
for (int i = 0; i < s.length(); i++) {
sb.append(s.charAt(i));
backtrace(digits, index + 1, sb, res);
sb.deleteCharAt(sb.length() - 1);
}
}
}
- 最小覆盖子串:给定一个字符串S和一个字符串T,在S中找出最短的包含T中所有字符的子串。可以使用滑动窗口算法来解决这个问题,通过移动左右指针来维护窗口的大小和位置,直到找到满足条件的最小覆盖子串。
public String minWindow(String s, String t) {
int[] tMap = new int[128]; // 统计T中每个字符的出现次数
for (int i = 0; i < t.length(); i++) {
tMap[t.charAt(i)]++;
}
int count = t.length(); // T中字符的个数
int left = 0, right = 0; // 滑动窗口的左右指针
int minLen = Integer.MAX_VALUE; // 最小子串的长度
int minStart = 0; // 最小子串的起始位置
while (right < s.length()) {
if (tMap[s.charAt(right)] > 0) { // 当前字符在T中出现
count--;
}
tMap[s.charAt(right)]--; // 将当前字符的出现次数减1
right++; // 右指针向右移动
while (count == 0) { // 如果当前窗口已经包含了T中的所有字符
if (right - left < minLen) { // 更新最小子串的长度和起始位置
minLen = right - left;
minStart = left;
}
if (tMap[s.charAt(left)] == 0) { // 左指针所指的字符在T中出现
count++;
}
tMap[s.charAt(left)]++; // 将左指针所指的字符的出现次数加1
left++; // 左指针向右移动
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(minStart, minStart + minLen);
}
其中,tMap数组用于统计T中每个字符的出现次数,count表示T中字符的个数,left和right是滑动窗口的左右指针,minLen和minStart分别表示最小子串的长度和起始位置。算法的时间复杂度为O(S+T),其中S和T分别为字符串S和字符串T的长度。
- 不同路径:给定一个m×n的网格,从左上角到右下角有多少条不同的路径。可以使用动态规划算法来解决这个问题,其中状态转移方程需要根据当前格子的左边格子和上面格子的路径数量来计算当前格子的路径数量。
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
// 初始化第一行和第一列的路径数量为1
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n; j++) {
dp[0][j] = 1;
}
// 计算每个格子的路径数量
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
// 返回右下角格子的路径数量
return dp[m - 1][n - 1];
}
其中,dp[i][j]表示从左上角到第i行第j列格子的不同路径数量。初始化第一行和第一列的路径数量为1,因为从左上角到第一行和第一列的格子只有一条路径。然后,通过状态转移方程dp[i][j] = dp[i - 1][j] + dp[i][j - 1]计算每个格子的路径数量,最后返回右下角格子的路径数量即可。
- 相交链表:给定两个单链表,判断它们是否相交,并返回它们的交点。可以使用双指针算法来解决这个问题,分别遍历两个链表,当其中一个链表遍历到末尾时,将它的指针指向另一个链表的头部,最终两个指针会在相交节点处相遇。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
其中,pA和pB分别代表两个链表的指针,它们遍历两个链表,如果一个指针到达链表末尾时,将它指向另一个链表的头部。如果两个链表相交,那么两个指针最终会在相交节点处相遇。如果两个链表不相交,那么两个指针最终会同时到达链表末尾,即pA和pB均为null。
- 二叉树的最近公共祖先:给定一个二叉树和两个节点,找到它们在树中的最近公共祖先。可以使用递归算法或迭代算法来解决这个问题,递归算法可以通过后序遍历的方式查找左右子树中是否包含目标节点,迭代算法则需要使用栈或队列来模拟递归过程。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) {
return root;
}
return left != null ? left : right;
}
其中,TreeNode 表示二叉树的节点,包含 val、left、right 三个属性。代码中,如果 root 为空或者 root 等于 p 或 q 中的任意一个节点,那么它就是最近公共祖先,直接返回 root。否则,分别在左子树和右子树中查找 p 和 q,如果在左右子树中都找到了,那么 root 就是最近公共祖先。如果只在其中一个子树中找到了,那么继续在那个子树中查找,直到找到最近公共祖先为止。
- 最长有效括号:给定一个只包含字符'('和')'的字符串,找到最长的有效括号子串的长度。可以使用栈或动态规划算法来解决这个问题,栈可以记录括号的下标位置,动态规划算法则需要定义状态和状态转移方程,以判断当前子串是否为有效括号串。
public int longestValidParentheses(String s) {
int n = s.length();
int[] dp = new int[n];
int maxLen = 0;
for (int i = 1; i < n; i++) {
if (s.charAt(i) == ')') {
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
dp[i] = dp[i - 1] + ((i - dp[i - 1] >= 2) ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxLen = Math.max(maxLen, dp[i]);
}
}
return maxLen;
}
该算法使用动态规划来计算最长有效括号长度,其中状态 dp[i] 表示以字符 s[i] 结尾的最长有效括号长度。当 s[i] 为右括号时,需要判断与它匹配的左括号的位置,从而计算 dp[i] 的值。具体来说:
如果 s[i-1] 为左括号,那么 s[i-1] 和 s[i] 构成了一个有效括号对,因此有 dp[i] = dp[i-2] + 2,其中 dp[i-2] 表示以 s[i-2] 结尾的最长有效括号长度。 如果 s[i-1] 为右括号,那么需要找到与之匹配的左括号,它的位置为 i-dp[i-1]-1。如果该位置是左括号,那么 s[i] 和该左括号构成了一个有效括号对,因此有 dp[i] = dp[i-1] + dp[i-dp[i-1]-2] + 2,其中 dp[i-1] 表示以 s[i-1] 结尾的最长有效括号长度,dp[i-dp[i-1]-2] 表示与左括号匹配的前一个字符的最长有效括号长度。 最终的最长有效括号长度为 max(dp)。时间复杂度为 ,空间复杂度为 。
- 零钱兑换:给定一组不同面额的硬币和一个总金额,计算出可以凑成总金额的最少硬币数量。可以使用贪心算法或动态规划算法来解决这个问题,贪心算法根据硬币面额从大到小进行贪心,动态规划算法则需要根据子问题的结果来确定当前子问题的解。
以下是使用动态规划算法解决零钱兑换问题的 Java 实现:
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
其中,dp[i] 表示凑成金额 i 需要的最少硬币数量,初始值为 amount+1,表示不可能凑成这个金额。状态转移方程为 dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1),表示将硬币 j 加入凑成金额 i 的方案中,需要使用 dp[i-coins[j]] 个硬币,因此需要加上一个硬币的数量 1,更新 dp[i] 的值为最小值。最终返回的值如果大于 amount,则表示无法凑成这个金额,返回 -1。
- 编辑距离:给定两个单词word1和word2,计算出将word1转换为word2所需的最少操作次数。可以使用动态规划算法来解决这个问题,其中状态转移方程需要根据当前字符是否相等来确定编辑距离的大小。
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
}
}
return dp[m][n];
}
其中,dp[i][j]表示将word1的前i个字符转换为word2的前j个字符所需要的最小编辑距离。在状态转移方程中,如果当前字符相等,则不需要进行编辑操作,即dp[i][j] = dp[i-1][j-1];否则需要进行插入、删除、替换中的一种操作,即dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1。最终结果为dp[m][n],其中m和n分别为word1和word2的长度。
- 字符串中的第一个唯一字符:给定一个字符串s,找到其中第一个不重复出现的字符,并返回它的下标。可以使用哈希表来记录每个字符出现的次数,然后再遍历一遍字符串找到第一个出现次数为1的字符。
public int firstUniqChar(String s) {
if (s == null || s.length() == 0) {
return -1;
}
int[] count = new int[26];
for (int i = 0; i < s.length(); i++) {
count[s.charAt(i) - 'a']++;
}
for (int i = 0; i < s.length(); i++) {
if (count[s.charAt(i) - 'a'] == 1) {
return i;
}
}
return -1;
}
首先判断字符串是否为空或长度为0,如果是则直接返回-1。 然后定义一个大小为26的数组count,用来记录每个字符出现的次数 接着遍历字符串s,对于每个字符c,将count[c - 'a']的值加1。 再次遍历字符串s,对于每个字符c,如果count[c - 'a']的值为1,说明这是第一次出现,直接返回它的下标i。 如果遍历完整个字符串都没有找到符合条件的字符,说明没有不重复出现的字符,返回-1。
- 字符串相乘:给定两个非负整数num1和num2,返回它们的乘积。可以使用竖式计算法来解决这个问题,从num2的末尾开始遍历每个数字,然后对num1进行乘法计算并将结果累加。
除了竖式计算法,还可以使用优化的竖式计算法,即利用数组存储每个数字相乘的结果,最后再处理进位。以下是java实现:
public String multiply(String num1, String num2) {
if (num1.equals("0") || num2.equals("0")) {
return "0";
}
int m = num1.length(), n = num2.length();
int[] res = new int[m + n];
for (int i = m - 1; i >= 0; i--) {
int x = num1.charAt(i) - '0';
for (int j = n - 1; j >= 0; j--) {
int y = num2.charAt(j) - '0';
res[i + j + 1] += x * y;
}
}
for (int i = m + n - 1; i > 0; i--) {
res[i - 1] += res[i] / 10;
res[i] %= 10;
}
int index = res[0] == 0 ? 1 : 0;
StringBuilder sb = new StringBuilder();
while (index < m + n) {
sb.append(res[index]);
index++;
}
return sb.toString();
}
其中,res数组存储每个数字相乘的结果,res[i+j+1]表示num1的第i位与num2的第j位相乘的结果应该累加到res[i+j+1]中。最后,需要处理进位,把res数组中每个元素的个位作为当前位,十位作为进位。需要注意的是,最终结果可能存在前导零,需要特殊处理。
- 验证二叉搜索树:给定一个二叉树,判断它是否是二叉搜索树。可以使用中序遍历算法来解决这个问题,因为中序遍历结果是有序的,所以可以判断遍历结果是否递增。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isValidBST(TreeNode root) {
return isValidBST(root, null, null);
}
private boolean isValidBST(TreeNode node, Integer lower, Integer upper) {
if (node == null) return true;
int val = node.val;
if (lower != null && val <= lower) return false;
if (upper != null && val >= upper) return false;
if (!isValidBST(node.left, lower, val)) return false;
if (!isValidBST(node.right, val, upper)) return false;
return true;
}
}
这是递归实现,使用了两个参数 lower 和 upper 分别表示该节点的左右子树的范围。在递归过程中判断当前节点的值是否在这个范围内,如果不在就说明不是二叉搜索树。
- 二叉树的最大深度:给定一个二叉树,返回它的最大深度。可以使用递归算法来解决这个问题,定义状态和状态转移方程,每次递归时将深度加1。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
int left = maxDepth(root.left);
int right = maxDepth(root.right);
return Math.max(left, right) + 1;
}
}
该代码使用了递归算法,对于每一个节点,分别递归其左右子树,并取左右子树的最大深度加1作为当前节点的最大深度。最后取所有节点的最大深度即为整个树的最大深度。
- 最长公共前缀:给定一个字符串数组,找到它们的最长公共前缀。可以使用纵向比较法来解决这个问题,从第一个字符开始逐列比较,如果不相同则返回前缀字符串。
class Solution {
public String longestCommonPrefix(String[] strs) {
if (strs == null || strs.length == 0) return "";
for (int i = 0; i < strs[0].length(); i++) {
char c = strs[0].charAt(i);
for (int j = 1; j < strs.length; j++) {
if (i == strs[j].length() || strs[j].charAt(i) != c)
return strs[0].substring(0, i);
}
}
return strs[0];
}
}
该代码使用了纵向比较法,从第一个字符开始逐列比较,如果有字符串的长度小于当前列数或该列字符不相同,则说明不是最长公共前缀,直接返回该前缀字符串即可。
- 接雨水:给定一个数组,表示一个高度图,求其中可以接多少雨水。可以使用双指针算法来解决这个问题,记录左右两边的最大高度,然后从两边向中间扫描。
class Solution {
public int trap(int[] height) {
int left = 0, right = height.length - 1;
int leftMax = 0, rightMax = 0;
int result = 0;
while (left < right) {
if (height[left] < height[right]) {
if (height[left] >= leftMax) {
leftMax = height[left];
} else {
result += leftMax - height[left];
}
left++;
} else {
if (height[right] >= rightMax) {
rightMax = height[right];
} else {
result += rightMax - height[right];
}
right--;
}
}
return result;
}
}
该代码实现了双指针算法,从两边向中间扫描,如果当前高度小于最高高度,则可以接雨水,累加到结果当中。如果当前高度大于最高高度,则更新最高高度。
- 最小路径和:给定一个m x n的网格,其中每个单元格都是非负整数,找到一条从左上角到右下角的路径,使得路径上的数字和最小。可以使用动态规划算法来解决这个问题,定义状态和状态转移方程。
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1; j < n; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[m - 1][n - 1];
}
}
该代码实现了动态规划算法,通过状态转移方程 dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j] 实现从左上角到右下角的路径,使得路径上的数字和最小。
- 无重复字符的最长子串:给定一个字符串,请找出其中不含有重复字符的最长子串的长度。可以使用滑动窗口算法来解决这个问题,记录左右两个指针,用哈希表来记录每个字符的出现位置。
可以使用滑动窗口算法来解决这个问题。首先创建一个哈希表来记录每个字符的出现位置。然后初始化两个指针,左指针和右指针,分别代表当前窗口的左端点和右端点。 接下来,移动右指针,如果当前字符没有在哈希表中出现过,则将其加入哈希表,并继续移动右指针。否则,如果当前字符已经在哈希表中出现过,则将左指针移动到该字符在哈希表中的最新位置的右侧。 每次移动后,都更新答案,即右指针减左指针加1。最终,答案即为不含有重复字符的最长子串的长度。
下面是 Java 代码实现:
class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Set<Character> set = new HashSet<>();
int ans = 0, i = 0, j = 0;
while (i < n && j < n) {
// try to extend the range [i, j]
if (!set.contains(s.charAt(j))) {
set.add(s.charAt(j++));
ans = Math.max(ans, j - i);
} else {
set.remove(s.charAt(i++));
}
}
return ans;
}
}
- 矩阵置零:给定一个m x n的矩阵,如果一个元素为0,则将其所在行和列的所有元素都设为0。可以使用两个布尔变量来记录第一行和第一列是否有0,然后遍历矩阵,用第一行和第一列来记录相应的行和列是否有0,最后再次遍历矩阵,将相应的行和列设为0。
public class Solution {
public void setZeroes(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
boolean firstRowZero = false, firstColZero = false;
// Check if the first row has a 0
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0) {
firstRowZero = true;
break;
}
}
// Check if the first column has a 0
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
firstColZero = true;
break;
}
}
// Use the first row and first column as markers
// to indicate if a row or column should be set to 0
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// Set the rows to 0
for (int i = 1; i < m; i++) {
if (matrix[i][0] == 0) {
for (int j = 1; j < n; j++) {
matrix[i][j] = 0;
}
}
}
// Set the columns to 0
for (int j = 1; j < n; j++) {
if (matrix[0][j] == 0) {
for (int i = 1; i < m; i++) {
matrix[i][j] = 0;
}
}
}
// Set the first row and first column to 0 if necessary
if (firstRowZero) {
for (int j = 0; j < n; j++) {
matrix[0][j] = 0;
}
}
if (firstColZero) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
}
}
- 合并两个有序链表:将两个有序链表合并为一个新的有序链表并返回。可以使用递归或迭代算法来解决这个问题,定义指针来遍历两个链表,并将较小的节点链接到新的链表中。
可以使用递归或迭代算法来解决这个问题。迭代算法通过使用指针遍历两个链表,并通过比较节点的值将较小的节点链接到新的链表中。递归算法通过递归地比较两个链表中的头节点,将较小的节点链接到新的链表中。
以下是使用迭代算法的 Java 代码实现:
/**
* 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) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
curr.next = l1;
l1 = l1.next;
} else {
curr.next = l2;
l2 = l2.next;
}
curr = curr.next;
}
curr.next = l1 != null ? l1 : l2;
return dummy.next;
}
}
- 对称二叉树:给定一个二叉树,检查它是否是镜像对称的。可以使用递归算法来解决这个问题,定义一个函数来比较左右子树是否镜像对称。
对称二叉树问题可以使用递归算法来解决。具体的,我们可以定义一个函数,该函数比较左右子树是否对称。
下面是一个 Java 的代码实现:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
return isMirror(root, root);
}
public boolean isMirror(TreeNode t1, TreeNode t2) {
if (t1 == null && t2 == null) return true;
if (t1 == null || t2 == null) return false;
return (t1.val == t2.val)
&& isMirror(t1.right, t2.left)
&& isMirror(t1.left, t2.right);
}
}
在这个代码中,我们首先定义了 isSymmetric 函数,该函数调用另一个函数 isMirror。isMirror 函数递归地比较两棵树是否对称,如果左右子树的值相等,且它们的左子树是右子树的镜像,并且它们的右子树是左子树的镜像,则返回 true,否则返回 false。
- 旋转数组:给定一个数组,将数组中的元素向右移动k个位置,其中k是非负数。可以使用反转算法来解决这个问题,先将整个数组反转,然后将前k个元素反转,最后将后面的元素反转。
public class Solution {
public void rotate(int[] nums, int k) {
k %= nums.length;
reverse(nums, 0, nums.length - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, nums.length - 1);
}
private void reverse(int[] nums, int start, int end) {
while (start < end) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
}
- 字符串转换整数 (atoi):实现一个函数,将字符串转换为整数。可以先去除字符串中的空格,并判断正负号,然后遍历字符串,将每个字符转换为数字,如果超过int范围,返回最大值或最小值。
class Solution {
public int myAtoi(String str) {
int index = 0, sign = 1, total = 0;
// 1. Remove spaces
while (index < str.length() && str.charAt(index) == ' ')
index ++;
// 2. Handle signs
if (index < str.length() && (str.charAt(index) == '+' || str.charAt(index) == '-')) {
sign = str.charAt(index) == '+' ? 1 : -1;
index ++;
}
// 3. Convert number and avoid overflow
while (index < str.length()) {
int digit = str.charAt(index) - '0';
if (digit < 0 || digit > 9) break;
// Check if total will be overflow after 10 times and add digit
if (Integer.MAX_VALUE/10 < total || Integer.MAX_VALUE/10 == total && Integer.MAX_VALUE %10 < digit)
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
total = 10 * total + digit;
index ++;
}
return total * sign;
}
}
- 二叉树的层序遍历:给定一个二叉树,按层遍历并返回节点值。可以使用队列来解决这个问题,先将根节点入队,然后每次将队头节点的左右子节点入队,并记录节点值。
import java.util.*;
/**
* 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>> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> levelList = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
levelList.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
result.add(levelList);
}
return result;
}
}
在这段代码中,我们首先判断根节点是否为空,如果为空,直接返回结果。否则,我们创建一个队列,并将根节点入队。然后,当队列不为空时,我们每次遍历队列的大小,遍历所有元素并将左右子节点入队。同时,我们将节点的值记录在levelList中。最后,将levelList添加到result中,并返回结果。
- K个一组翻转链表:给定一个链表,每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) {
ListNode curr = head;
int count = 0;
while (curr != null && count != k) {
curr = curr.next;
count++;
}
if (count == k) {
curr = reverseKGroup(curr, k);
while (count > 0) {
ListNode temp = head.next;
head.next = curr;
curr = head;
head = temp;
count--;
}
head = curr;
}
return head;
}
}
该代码使用递归算法来实现链表的翻转。首先,遍历链表,找到第k个节点。然后,递归翻转后面的部分,并将前k个节点和后面的部分连接起来。最后,返回翻转后的链表。
- 最小栈:设计一个支持push、pop、top操作,并能在常数时间内检索最小元素的栈。可以使用辅助栈来记录每个状态的最小值,保证在常数时间内获取最小值。
实现最小栈的思路如下: 1.创建两个栈,一个栈用于存储数据,另一个栈用于存储每个状态的最小值。 2.在push操作中,首先将数据元素压入数据栈,然后比较该数据元素与当前最小栈栈顶元素的大小。如果该数据元素比当前最小栈栈顶元素小,则将该数据元素压入最小栈。 3.在pop操作中,先从数据栈弹出栈顶元素,再判断该元素是否等于当前最小栈的栈顶元素,如果相等,则也从最小栈弹出栈顶元素。 4.在top操作中,直接返回数据栈的栈顶元素。 5.在getMin操作中,直接返回最小栈的栈顶元素。
下面是代码示例:
import java.util.Stack;
class MinStack {
private Stack<Integer> dataStack;
private Stack<Integer> minStack;
public MinStack() {
dataStack = new Stack<>();
minStack = new Stack<>();
}
public void push(int x) {
dataStack.push(x);
if (minStack.empty() || x <= minStack.peek()) {
minStack.push(x);
}
}
public void pop() {
if (dataStack.pop().equals(minStack.peek())) {
minStack.pop();
}
}
public int top() {
return dataStack.peek();
}
public int getMin() {
return minStack.peek();
}
}
- 合并K个有序链表:给定k个有序链表,将它们合并成一个有序链表。可以使用分治算法来解决这个问题,将k个链表分为两部分,递归合并两部分,直到只剩下一个链表。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
if (lists.length == 1) return lists[0];
int interval = 1;
while (interval < lists.length) {
for (int i = 0; i < lists.length - interval; i += interval * 2) {
lists[i] = mergeTwoLists(lists[i], lists[i + interval]);
}
interval *= 2;
}
return lists[0];
}
private ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
cur.next = list1;
list1 = list1.next;
} else {
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
if (list1 != null) {
cur.next = list1;
}
if (list2 != null) {
cur.next = list2;
}
return dummy.next;
}
}
这段代码使用了分治算法,先将K个链表不断地两两合并,最终合并成一个链表。具体的,首先将K个链表从中间分成两部分,分别递归合并这两部分,直到只剩下一个链表。
- 最长不含重复字符的子串:给定一个字符串,找出不含重复字符的最长子串的长度。可以使用滑动窗口算法来解决这个问题,维护一个窗口,当窗口中的字符没有重复时,右边界向右移动,否则左边界向右移动。
实现一个最长不含重复字符的子串的长度的算法,可以使用滑动窗口的思想。 滑动窗口算法的基本思想是使用两个指针left和right,代表窗口的左边界和右边界。当窗口中的字符没有重复时,right向右移动,并统计窗口的长度。当出现重复字符时,left向右移动,直到窗口中不含有重复字符为止。 具体实现可以使用一个哈希表,记录窗口中每个字符最后出现的位置,以便快速判断重复字符。
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int ans = 0;
Map<Character, Integer> map = new HashMap<>();
for (int i = 0, j = 0; j < n; j++) {
if (map.containsKey(s.charAt(j))) {
i = Math.max(map.get(s.charAt(j)), i);
}
ans = Math.max(ans, j - i + 1);
map.put(s.charAt(j), j + 1);
}
return ans;
}
上面的代码中,使用了Map映射每个字符最后出现的位置,然后使用两个指针i和j,从左往右遍历字符串,每次右移j,判断当前字符是否在映射表中,如果已经出现过,那么移动左边界i到该字符最后出现的位置的下一个位置。每次移动j,更新当前字符的
- 盛最多水的容器:给定n个非负整数a1,a2,...,an,其中每个点的坐标表示(i,ai)。找出两条线,它们与x轴一起构成一个容器,使容器中的水最多。可以使用双指针算法来解决这个问题,左右指针分别指向容器的左右边界,每次移动高度小的指针,更新容积。
class Solution {
public int maxArea(int[] height) {
int maxArea = 0;
int left = 0, right = height.length - 1;
while (left < right) {
maxArea = Math.max(maxArea, Math.min(height[left], height[right]) * (right - left));
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
}
其中,变量 left 和 right 分别代表容器的左右边界,在每次循环中,通过比较两边高度,将高度小的一边向中间移动,并更新容积的最大值。
- Pow(x, n):实现pow(x, n),即计算x的n次幂函数。可以使用递归算法来解决这个问题,将问题拆分成pow(x, n/2)和pow(x, n-n/2)两个子问题。
class Solution {
public double myPow(double x, int n) {
if (n == 0) return 1;
if (n < 0) {
n = -n;
x = 1 / x;
}
return (n % 2 == 0) ? myPow(x * x, n / 2) : x * myPow(x * x, n / 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> result = new ArrayList<>();
inorderTraversalHelper(root, result);
return result;
}
private void inorderTraversalHelper(TreeNode root, List<Integer> result) {
if (root == null) {
return;
}
inorderTraversalHelper(root.left, result);
result.add(root.val);
inorderTraversalHelper(root.right, result);
}
}
下面是栈实现的代码示例:
/**
* 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> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode curr = root;
while (curr != null || !stack.isEmpty()) {
while (curr != null) {
stack.push(curr);
curr = curr.left;
}
curr = stack.pop();
result.add(curr.val);
curr = curr.right;
}
return result;
}
}
- 岛屿数量:给定一个由‘1’(陆地)和‘0’(水)组成的二维网格,计算岛屿的数量。可以使用深度优先搜索(DFS)或广度优先搜索(BFS)来解决这个问题。
下面是使用深度优先搜索的 Java 代码实现:
class Solution {
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int numIslands = 0;
int rows = grid.length;
int cols = grid[0].length;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == '1') {
numIslands++;
dfs(grid, i, j);
}
}
}
return numIslands;
}
private void dfs(char[][] grid, int i, int j) {
int rows = grid.length;
int cols = grid[0].length;
if (i < 0 || i >= rows || j < 0 || j >= cols || grid[i][j] == '0') {
return;
}
grid[i][j] = '0';
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
}
}
下面是使用广度优先搜索的 Java 代码实现:
class Solution {
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int m = grid.length, n = grid[0].length;
int count = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == '1') {
count++;
bfs(grid, i, j);
}
}
}
return count;
}
private void bfs(char[][] grid, int i, int j) {
int m = grid.length, n = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[] {i, j});
grid[i][j] = '0';
int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
while (!queue.isEmpty()) {
int[] cur = queue.poll();
for (int[] dir : dirs) {
int x = cur[0] + dir[0];
int y = cur[1] + dir[1];
if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == '1') {
queue.offer(new int[] {x, y});
grid[x][y] = '0';
}
}
}
}
}
- 寻找两个有序数组的中位数:给定两个大小为m和n的有序数组,找出这两个数组合并后的中位数。可以使用归并排序来解决这个问题。
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int m = nums1.length;
int n = nums2.length;
if (m > n) {
int[] temp = nums1; nums1 = nums2; nums2 = temp;
int tmp = m; m = n; n = tmp;
}
int iMin = 0, iMax = m, halfLen = (m + n + 1) / 2;
while (iMin <= iMax) {
int i = (iMin + iMax) / 2;
int j = halfLen - i;
if (i < iMax && nums2[j-1] > nums1[i]){
iMin = i + 1;
}
else if (i > iMin && nums1[i-1] > nums2[j]) {
iMax = i - 1;
}
else {
int maxLeft = 0;
if (i == 0) { maxLeft = nums2[j-1]; }
else if (j == 0) { maxLeft = nums1[i-1]; }
else { maxLeft = Math.max(nums1[i-1], nums2[j-1]); }
if ( (m + n) % 2 == 1 ) { return maxLeft; }
int minRight = 0;
if (i == m) { minRight = nums2[j]; }
else if (j == n) { minRight = nums1[i]; }
else { minRight = Math.min(nums2[j], nums1[i]); }
return (maxLeft + minRight) / 2.0;
}
}
return 0.0;
}
- 翻转二叉树:给定一个二叉树,将其翻转,即左子树变成右子树,右子树变成左子树。可以使用递归或迭代来实现。
下面是使用递归实现二叉树翻转的代码:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode right = invertTree(root.right);
TreeNode left = invertTree(root.left);
root.left = right;
root.right = left;
return 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 invertTree(TreeNode root) {
if (root == null) {
return null;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
TreeNode left = node.left;
node.left = node.right;
node.right = left;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
return root;
}
}
- 单词搜索:给定一个二维字符数组和一个单词,判断该单词是否存在于数组中。可以使用回溯算法来解决这个问题。
以下是一种使用回溯算法的 Java 实现:
class Solution {
public boolean exist(char[][] board, String word) {
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (dfs(board, word, i, j, 0)) return true;
}
}
return false;
}
private boolean dfs(char[][] board, String word, int i, int j, int k) {
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length) return false;
if (board[i][j] != word.charAt(k)) return false;
if (k == word.length() - 1) return true;
char temp = board[i][j];
board[i][j] = '*';
boolean found = dfs(board, word, i + 1, j, k + 1) ||
dfs(board, word, i - 1, j, k + 1) ||
dfs(board, word, i, j + 1, k + 1) ||
dfs(board, word, i, j - 1, k + 1);
board[i][j] = temp;
return found;
}
}
在此实现中,我们首先遍历板上的每个单元格。对于每个单元格,我们使用 dfs 函数来检查当前单元格是否是我们要查找的单词的起点。如果是,我们使用回溯在所有四个可能的方向上继续搜索
- 最长上升子序列:给定一个无序的整数数组,找到其中最长上升子序列的长度。可以使用动态规划或贪心算法来解决这个问题。
动态规划:
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
if (n == 0) return 0;
int[] dp = new int[n];
int res = 1;
for (int i = 0; i < n; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
}
贪心:
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
if (n == 0) return 0;
int[] tails = new int[n];
int res = 0;
for (int num : nums) {
int left = 0, right = res;
while (left < right) {
int mid = left + (right - left) / 2;
if (tails[mid] < num) left = mid + 1;
else right = mid;
}
tails[left] = num;
if (left == res) res++;
}
return res;
}
}
- 数组中第K大的元素:给定一个无序的整数数组,找到其中第K大的元素。可以使用快速选择算法来解决这个问题。
public class Solution {
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, 0, nums.length - 1, nums.length - k);
}
private int quickSelect(int[] nums, int start, int end, int k) {
int pivot = nums[end];
int left = start;
int right = end;
while (true) {
while (nums[left] < pivot && left < right) {
left++;
}
while (nums[right] >= pivot && right > left) {
right--;
}
if (left == right) {
break;
}
swap(nums, left, right);
}
swap(nums, left, end);
if (left == k) {
return nums[left];
} else if (left < k) {
return quickSelect(nums, left + 1, end, k);
} else {
return quickSelect(nums, start, left - 1, k);
}
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
这里使用快速选择算法,其基于快排算法,通过不断分治来确定目标元素在数组中的位置。
- 实现 LRU 缓存机制:设计一个 LRU 缓存机制,要求支持 get 和 put 操作,get 操作可以获得缓存中的值,put 操作可以插入或修改缓存中的值。可以使用哈希表和双向链表来实现。
import java.util.LinkedHashMap;
import java.util.Map;
class LRUCache {
private LinkedHashMap<Integer, Integer> map;
private final int CAPACITY;
public LRUCache(int capacity) {
CAPACITY = capacity;
map = new LinkedHashMap<Integer, Integer>(capacity, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > CAPACITY;
}
};
}
public int get(int key) {
return map.getOrDefault(key, -1);
}
public void put(int key, int value) {
map.put(key, value);
}
}
这是一种使用 Java 实现 LRU 缓存机制的方法,它使用了 Java 提供的 LinkedHashMap 类,这个类继承自 HashMap 类,它有一个构造函数可以指定容量,并可以在每次 put 操作时判断缓存是否已满,如果满了,则删除最老的缓存数据。
- 不同的二叉搜索树:给定一个整数 n,求以 1...n 为节点组成的二叉搜索树有多少种。可以使用动态规划来解决这个问题。
下面是使用动态规划来解决这个问题的 Java 代码实现:
class Solution {
public int numTrees(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i; j++) {
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
}
该代码实现通过定义一个数组 dp 来记录以 1...n 为节点组成的二叉搜索树的数量。对于每个数字 i,从 1 到 i 枚举其为根节点的情况,把二叉搜索树的左子树和右子树的数量相乘并累加到 dp[i] 上。
- 用栈实现队列:使用栈实现队列的下列操作:push(x),pop(),peek() 和 empty()。
这可以通过使用两个栈实现。一个栈用于进行 push 操作,另一个栈用于进行 pop 操作。对于每一次的 push 操作,我们将数据插入第一个栈,对于每一次的 pop 操作,我们将数据从第二个栈中弹出。当第二个栈为空时,我们将第一个栈中的所有数据弹出并插入第二个栈。这样,第二个栈就会保存第一个栈中从前往后的数据,这正好符合队列的先进先出的顺序。
以下是 Java 代码实现:
import java.util.Stack;
public class MyQueue {
private Stack<Integer> stack1;
private Stack<Integer> stack2;
/** Initialize your data structure here. */
public MyQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
/** Push element x to the back of queue. */
public void push(int x) {
stack1.push(x);
}
/** Removes the element from in front of queue and returns that element. */
public int pop() {
if (stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
/** Get the front element. */
public int peek() {
if (stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
/** Returns whether the queue is empty. */
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}
- 用两个栈实现队列:使用两个栈实现队列的下列操作:push(x),pop(),peek() 和 empty()。
可以使用两个栈实现队列:一个栈用于存储元素,另一个栈用于模拟队列的出队操作。具体实现方法如下:
push(x) 操作:将元素 x 推入存储栈。
pop() 操作:如果模拟栈为空,则将存储栈中的元素依次弹出并压入模拟栈,再弹出模拟栈的栈顶元素。如果模拟栈不为空,直接弹出模拟栈的栈顶元素。
peek() 操作:如果模拟栈为空,则将存储栈中的元素依次弹出并压入模拟栈,再返回模拟栈的栈顶元素。如果模拟栈不为空,直接返回模拟栈的栈顶元素。
empty() 操作:如果两个栈都为空,则返回 true,否则返回 false。
代码如下:
import java.util.Stack;
class MyQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
/** Initialize your data structure here. */
public MyQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
/** Push element x to the back of queue. */
public void push(int x) {
stack1.push(x);
}
/** Removes the element from in front of queue and returns that element. */
public int pop() {
if (stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.pop();
}
/** Get the front element. */
public int peek() {
if (stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
}
return stack2.peek();
}
/** Returns whether the queue is empty. */
public boolean empty() {
return stack1.isEmpty() && stack2.isEmpty();
}
}
- 旋转图像:给定一个 n × n 的二维矩阵表示一个图像,将图像顺时针旋转 90 度。可以先将矩阵转置,然后再翻转每一行来实现。
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 转置矩阵
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
// 翻转每一行
for (int i = 0; i < n; i++) {
for (int j = 0; j < n / 2; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[i][n - j - 1];
matrix[i][n - j - 1] = temp;
}
}
}
}
在上面的代码中,首先我们使用两重循环转置矩阵,然后再使用另一重循环翻转每一行。
- 颠倒二进制位:给定一个无符号整数,将其二进制数颠倒过来并返回。
public int reverseBits(int n) {
int result = 0;
for (int i = 0; i < 32; i++) {
result += n & 1;
n >>>= 1;
if (i < 31) {
result <<= 1;
}
}
return result;
}
这段代码定义了一个名为 reverseBits 的方法,该方法接受一个无符号整数作为参数,并返回该数的二进制颠倒结果。它首先定义一个 result 变量,并使用一个 for 循环来迭代 32 次。每次循环都会将 n 的最低有效位移动到 result 的最高有效位。最后,它会返回 result 的值。
- 两数之和 II - 输入有序数组:给定一个已按照升序排列的整数数组 numbers,请你从数组中找出两个数满足相加之和等于目标数 target。
这道题可以使用双指针的方法来解决。首先,设置两个指针,分别指向数组的头和尾,然后不断比较这两个数的和与目标数的大小关系,如果和小于目标数,则将左指针右移,如果和大于目标数,则将右指针左移,直到找到相加之和等于目标数的两个数为止。
下面是 Java 代码实现:
class Solution {
public int[] twoSum(int[] numbers, int target) {
int[] result = new int[2];
int left = 0, right = numbers.length - 1;
while (left < right) {
int sum = numbers[left] + numbers[right];
if (sum == target) {
result[0] = left + 1;
result[1] = right + 1;
break;
} else if (sum < target) {
left++;
} else {
right--;
}
}
return result;
}
}
- 爬楼梯:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
你可以使用递归或动态规划来解决这个问题。
使用递归:
public int climbStairs(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
return climbStairs(n - 1) + climbStairs(n - 2);
}
使用动态规划:
public int climbStairs(int n) {
if (n == 1) {
return 1;
}
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
- 买卖股票的最佳时机:给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。可以完成任意次交易,但不能同时参与多个交易,即必须在再次购买前出售掉之前的股票。
class Solution {
public int maxProfit(int[] prices) {
int maxProfit = 0;
for (int i = 1; i < prices.length; i++) {
if (prices[i] > prices[i - 1]) {
maxProfit += prices[i] - prices[i - 1];
}
}
return maxProfit;
}
}
这个算法通过遍历数组,如果当前天的价格比前一天的价格高,就累加利润。因为数组已经按升序排列,所以这样可以保证我们的利润是最大的。
- 排序链表:在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
这是如何对链表进行排序的一种 Java 实现方式,使用归并排序的思想:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode right = sortList(slow.next);
slow.next = null;
ListNode left = sortList(head);
return merge(left, right);
}
private ListNode merge(ListNode left, ListNode right) {
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
while (left != null && right != null) {
if (left.val < right.val) {
cur.next = left;
left = left.next;
} else {
cur.next = right;
right = right.next;
}
cur = cur.next;
}
if (left != null) {
cur.next = left;
} else {
cur.next = right;
}
return dummy.next;
}
}
在上面的代码中,我们先使用快慢指针将链表平分为两个部分,然后对每一部分分别递归地调用 sortList 方法进行排序,最后再合并两个排序好的链表,实现链表的排序。
- 拓扑排序:给定一个有向无环图,进行拓扑排序,输出拓扑排序的结果。
拓扑排序是一种特殊的排序方式,它主要用于有向无环图(DAG)中的顶点排序。拓扑排序通过将每个顶点的入度减一,从而遍历图中的顶点。
以下是使用队列来实现拓扑排序的 Java 代码:
import java.util.*;
class Solution {
public int[] topologicalSort(int numCourses, int[][] prerequisites) {
int[] indegrees = new int[numCourses];
List<Integer>[] adjacency = new ArrayList[numCourses];
for (int i = 0; i < numCourses; i++) {
adjacency[i] = new ArrayList<>();
}
for (int[] prerequisite : prerequisites) {
indegrees[prerequisite[0]]++;
adjacency[prerequisite[1]].add(prerequisite[0]);
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (indegrees[i] == 0) {
queue.offer(i);
}
}
int[] result = new int[numCourses];
int index = 0;
while (!queue.isEmpty()) {
int course = queue.poll();
result[index++] = course;
for (int i : adjacency[course]) {
if (--indegrees[i] == 0) {
queue.offer(i);
}
}
}
return index == numCourses ? result : new int[0];
}
}
上述代码使用队列来实现拓扑排序,首先遍历所有课程,如果某个课程的入读为零,则将其加入队列中。然后每次取出队首元素,并将与其相关的课程的入读减一,如果入读为零,则加入队列中,直到队列为空为止。
- 反转字符串中的单词 III:给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。
class Solution {
public String reverseWords(String s) {
String[] words = s.split(" ");
StringBuilder result = new StringBuilder();
for (String word : words) {
result.append(new StringBuilder(word).reverse().toString()).append(" ");
}
return result.toString().trim();
}
}
- 计算器:实现一个基本的计算器来计算一个简单的字符串表达式的值。
import java.util.Stack;
public class Calculator {
public int calculate(String s) {
Stack<Integer> numStack = new Stack<>();
Stack<Character> opStack = new Stack<>();
int num = 0;
char sign = '+';
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
num = num * 10 + c - '0';
}
if (!Character.isDigit(c) && c != ' ' || i == s.length() - 1) {
if (sign == '+') {
numStack.push(num);
} else if (sign == '-') {
numStack.push(-num);
} else if (sign == '*') {
numStack.push(numStack.pop() * num);
} else if (sign == '/') {
numStack.push(numStack.pop() / num);
}
sign = c;
num = 0;
}
}
int res = 0;
while (!numStack.isEmpty()) {
res += numStack.pop();
}
return res;
}
}
- 罗马数字转整数:罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。将罗马数字转换成整数。
class Solution {
public int romanToInt(String s) {
int res = 0;
for (int i = 0; i < s.length(); i++) {
int s1 = getValue(s.charAt(i));
if (i + 1 < s.length()) {
int s2 = getValue(s.charAt(i + 1));
if (s1 >= s2) {
res = res + s1;
} else {
res = res + s2 - s1;
i++;
}
} else {
res = res + s1;
i++;
}
}
return res;
}
private int getValue(char c) {
switch (c) {
case 'I': return 1;
case 'V': return 5;
case 'X': return 10;
case 'L': return 50;
case 'C': return 100;
case 'D': return 500;
case 'M': return 1000;
default: return 0;
}
}
}
- 实现 Trie (前缀树):实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。
class Trie {
private TrieNode root;
/** Initialize your data structure here. */
public Trie() {
root = new TrieNode();
}
/** Inserts a word into the trie. */
public void insert(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
char currentChar = word.charAt(i);
if (!node.containsKey(currentChar)) {
node.put(currentChar, new TrieNode());
}
node = node.get(currentChar);
}
node.setEnd();
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
TrieNode node = searchPrefix(word);
return node != null && node.isEnd();
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
TrieNode node = searchPrefix(prefix);
return node != null;
}
private TrieNode searchPrefix(String word) {
TrieNode node = root;
for (int i = 0; i < word.length(); i++) {
char curLetter = word.charAt(i);
if (node.containsKey(curLetter)) {
node = node.get(curLetter);
} else {
return null;
}
}
return node;
}
class TrieNode {
private TrieNode[] links;
private final int R = 26;
private boolean isEnd;
public TrieNode() {
links = new TrieNode[R];
}
public boolean containsKey(char ch) {
return links[ch -'a'] != null;
}
public TrieNode get(char ch) {
return links[ch -'a'];
}
public void put(char ch, TrieNode node) {
links[ch -'a'] = node;
}
public void setEnd() {
isEnd = true;
}
public boolean isEnd() {
return isEnd;
}
}
}
- 三数之和:给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}
}
- 链表相交:编写一个程序,找到两个单链表相交的起始节点。
/**
* 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;
int lenB = 0;
ListNode nodeA = headA;
ListNode nodeB = headB;
while (nodeA != null) {
lenA++;
nodeA = nodeA.next;
}
while (nodeB != null) {
lenB++;
nodeB = nodeB.next;
}
nodeA = headA;
nodeB = headB;
if (lenA > lenB) {
int steps = lenA - lenB;
while (steps > 0) {
nodeA = nodeA.next;
steps--;
}
} else {
int steps = lenB - lenA;
while (steps > 0) {
nodeB = nodeB.next;
steps--;
}
}
while (nodeA != null && nodeB != null) {
if (nodeA == nodeB) {
return nodeA;
}
nodeA = nodeA.next;
nodeB = nodeB.next;
}
return null;
}
}
- 寻找旋转排序数组中的最小值:假设按照升序排序的数组在预先未知的某个点上进行了旋转,请找出其中最小的元素。
这是一道二分查找的题目。用二分查找的方法可以在 logN 的时间复杂度内找到最小值。
实现思路:
首先判断数组是否存在旋转,如果没有,说明数组是排序的,最小值就是数组的第一个元素。 如果存在旋转,那么在旋转点的左侧和右侧的数组分别是有序的。在二分查找的时候判断中间元素的值与最右侧元素的值的大小关系,如果中间元素的值比最右侧元素的值大,说明最小值在中间元素的左侧,否则最小值在中间元素的右侧。
示例代码如下:
class Solution {
public int findMin(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] > nums[right]) {
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
}
}
- 螺旋矩阵:给定一个包含 m x n 个元素的矩阵(m 行, n 列),按照从外向里以顺时针的顺序依次打印出每一个元素。
这是一道经典的数组遍历题目。解决该问题的方法是利用四个指针(left、right、top、bottom)分别指向矩阵的四个边界,然后按照顺时针顺序向内移动指针,并打印当前指针指向的元素。当元素全部打印完成后,程序结束。
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> result = new ArrayList<>();
if (matrix == null || matrix.length == 0) {
return result;
}
int rows = matrix.length;
int cols = matrix[0].length;
int left = 0, right = cols - 1, top = 0, bottom = rows - 1;
while (result.size() < rows * cols) {
for (int i = left; i <= right && result.size() < rows * cols; i++) {
result.add(matrix[top][i]);
}
top++;
for (int i = top; i <= bottom && result.size() < rows * cols; i++) {
result.add(matrix[i][right]);
}
right--;
for (int i = right; i >= left && result.size() < rows * cols; i--) {
result.add(matrix[bottom][i]);
}
bottom--;
for (int i = bottom; i >= top && result.size() < rows * cols; i--) {
result.add(matrix[i][left]);
}
left++;
}
return result;
}
}
- 最长无重复子串:给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
下面是一种解决此问题的方法:滑动窗口法。
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Set<Character> set = new HashSet<>();
int ans = 0, i = 0, j = 0;
while (i < n && j < n) {
// try to extend the range [i, j]
if (!set.contains(s.charAt(j))){
set.add(s.charAt(j++));
ans = Math.max(ans, j - i);
}
else {
set.remove(s.charAt(i++));
}
}
return ans;
}
该代码使用了双指针法和 HashSet 数据结构。从左到右遍历字符串,并尝试扩展子字符串的右边界。当遇到重复字符时,就缩小左边界,直到不再有重复字符。