LeetCode算法小抄--数组
Posted 不懂开发的程序猿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCode算法小抄--数组相关的知识,希望对你有一定的参考价值。
LeetCode算法小抄--数组
⚠申明: 未经许可,禁止以任何形式转载,若要引用,请标注链接地址。 全文共计3077字,阅读大概需要3分钟
🌈更多学习内容, 欢迎👏关注👀文末我的个人微信公众号:不懂开发的程序猿
个人网站:https://jerry-jy.co/
数组
1、双指针
双指针技巧主要分为两类:左右指针和快慢指针。
快慢指针
26. 删除有序数组中的重复项
给你一个 升序排列 的数组 nums
,请你 原地删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
由于在某些语言中不能改变数组的长度,所以必须将结果放在数组 nums
的第一部分。更规范地说,如果在删除重复项之后有 k
个元素,那么 nums
的前 k
个元素应该保存最终结果。
将最终结果插入 nums
的前 k
个位置后返回 k
。
不要使用额外的空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
class Solution
public int removeDuplicates(int[] nums)
if(nums.length == 0) return 0;
int slow = 0, fast = 0;
while(fast < nums.length)
if(nums[fast] != nums[slow])
slow++;
// 维护 nums[0..slow] 无重复
nums[slow] = nums[fast];
fast++;
// 数组长度为 索引 + 1
return slow + 1;
扩展:
83. 删除排序链表中的重复元素
给定一个已排序的链表的头 head
, 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
/**
* 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 deleteDuplicates(ListNode head)
if(head == null) return null;
ListNode slow, fast;
slow = fast = head;
while(fast != null)
if(fast.val != slow.val)
// nums[slow] = nums[fast];
slow.next = fast;
// slow++;
slow = slow.next;
// fast++
fast = fast.next;
// 断开与后面重复元素的连接
slow.next = null;
return head;
总结:类比上面的有序数组原地去重,有个细节地方:链表去重要先赋值,再slow++
27. 移除元素
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1)
额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
class Solution
public int removeElement(int[] nums, int val)
if(nums.length == 0) return 0;
int slow = 0, fast = 0;
while(fast < nums.length)
if(nums[fast] != val)
nums[slow] = nums[fast];
slow++;
fast++;
return slow;
注意:这里和有序数组去重的解法有一个细节,我们这里是先给 nums[slow]
赋值然后再给 slow++
,这样可以保证 nums[0..slow-1]
是不包含值为 val
的元素的,最后的结果数组长度就是 slow
,slow下标记得不用+1
扩展:
283. 移动零
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
思路:题目让我们将所有 0 移到最后,其实就相当于移除 nums
中的所有 0,然后再把后面的元素都赋值为 0 即可。
class Solution
public void moveZeroes(int[] nums)
// 去除 nums 中的所有 0,返回不含 0 的数组长度
int len = removeElement(nums, 0);
// 将 nums[len...] 的元素赋值为 0
for(; len < nums.length; len++)
nums[len] = 0;
private int removeElement(int[] nums, int val)
if(nums.length == 0) return 0;
int slow = 0, fast = 0;
while(fast < nums.length)
if(nums[fast] != val)
nums[slow] = nums[fast];
slow++;
fast++;
return slow;
两数之和
167. 两数之和 II - 输入有序数组
给你一个下标从 1 开始的整数数组 numbers
,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target
的两个数。如果设这两个数分别是 numbers[index1]
和 numbers[index2]
,则 1 <= index1 < index2 <= numbers.length
。
以长度为 2 的整数数组 [index1, index2]
的形式返回这两个整数的下标 index1
和 index2
。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
思路:只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left
和 right
就可以调整 sum
的大小:
class Solution
public int[] twoSum(int[] numbers, int target)
// 一左一右两个指针相向而行
int left = 0, right = numbers.length - 1;
while(left < right)
int sum = numbers[left] + numbers[right];
if(sum == target)
// 题目要求的索引是从 1 开始的
return new int[]left+1, right+1;
else if (sum < target)
left++;// 让 sum 大一点
else if (sum > target)
right--;// 让 sum 小一点
return new int[]-1, -1;
反转数组
344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s
的形式给出。
不要给另外的数组分配额外的空间,你必须**原地修改输入数组**、使用 O(1) 的额外空间解决这一问题。
class Solution
public void reverseString(char[] s)
// 一左一右两个指针相向而行
int left = 0, right = s.length - 1;
while(left < right)
// 交换 s[left] 和 s[right]
char temp = s[right];
s[right] = s[left];
s[left] = temp;
right--;
left++;
回文串判断
判断一个字符串是不是回文串
boolean isPalindrome(String s)
// 一左一右两个指针相向而行
int left = 0, right = s.length() - 1;
while (left < right)
if (s.charAt(left) != s.charAt(right))
return false;
left++;
right--;
return true;
5. 最长回文子串
给你一个字符串 s
,找到 s
中最长的回文子串。
如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
找回文串的难点在于,回文串的的长度可能是奇数也可能是偶数,解决该问题的核心是从中心向两端扩散的双指针技巧。
如果回文串的长度为奇数,则它有一个中心字符;如果回文串的长度为偶数,则可以认为它有两个中心字符。
class Solution
public String longestPalindrome(String s)
String res = "";
for(int i = 0; i < s.length(); i++)
// 以 s[i] 为中心的最长回文子串
String s1 = palindrome(s, i, i);
// 以 s[i] 和 s[i+1] 为中心的最长回文子串
String s2 = palindrome(s, i, i+1);
// 没看懂这里???
res = res.length() > s1.length() ? res : s1;
res = res.length() > s2.length() ? res : s2;
return res;
// 在 s 中寻找以 s[l] 和 s[r] 为中心的最长回文串
private String palindrome(String s, int l, int r)
// 防止索引越界
while(l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r))
// 双指针,向两边展开
l--;
r++;
// 返回以 s[l] 和 s[r] 为中心的最长回文串
return s.substring(l + 1, r);
总结:最长回文子串使用的左右指针和之前题目的左右指针有一些不同:之前的左右指针都是从两端向中间相向而行,而回文子串问题则是让左右指针从中心向两端扩展。
2、数组求和技巧
前缀和数组
前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。
303. 区域和检索 - 数组不可变
给定一个整数数组 nums
,处理以下类型的多个查询:
计算索引 left
和 right
(包含 left
和 right
)之间的 nums
元素的 和 ,其中 left <= right
1、一般人的解法:
class NumArray
private int[] arr;
public NumArray(int[] nums)
arr = nums;
public int sumRange(int left, int right)
int res = 0;
for(int i = left; i <= right; i++)
res += arr[i];
return res;
/**
* Your NumArray object will be instantiated and called as such:
* NumArray obj = new NumArray(nums);
* int param_1 = obj.sumRange(left,right);
*/
这样,可以达到效果,但是效率很差,因为 sumRange
方法会被频繁调用,而它的时间复杂度是 O(N)
,其中 N
代表 nums
数组的长度。
这道题的最优解法是使用前缀和技巧,将 sumRange
函数的时间复杂度降为 O(1)
,说白了就是不要在 sumRange
里面用 for 循环
class NumArray
// 前缀和数组
private int[] preSum;
/* 输入一个数组,构造前缀和 */
public NumArray(int[] nums)
// 前缀和数组比原始数组索引多1,因为一个都不累加就是0
preSum = new int[nums.length + 1];
// 计算 nums 的累加和
for(int i = 1; i < preSum.length; i++)
preSum[i] = preSum[i -1] + nums[i - 1];
public int sumRange(int left, int right)
//如果我想求索引区间 [1, 4] 内的所有元素之和,就可以通过 preSum[5] - preSum[1]
return preSum[right + 1] - preSum[left];
304. 二维区域和检索 - 矩阵不可变
给定一个二维矩阵 matrix
,以下类型的多个请求:
- 计算其子矩形范围内元素的总和,该子矩阵的 左上角 为
(row1, col1)
,右下角 为(row2, col2)
。
实现 NumMatrix
类:
NumMatrix(int[][] matrix)
给定整数矩阵matrix
进行初始化int sumRegion(int row1, int col1, int row2, int col2)
返回 左上角(row1, col1)
、右下角(row2, col2)
所描述的子矩阵的元素 总和 。
class NumMatrix
// 定义:preSum[i][j] 记录 matrix 中子矩阵 [0, 0, i-1, j-1] 的元素和
private int[][] preSum;
public NumMatrix(int[][] matrix)
int m = matrix.length, n = matrix[0].length;
if (m == 0 || n == 0) return;
// 构造前缀和矩阵
preSum = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
// 计算每个矩阵 [0, 0, i, j] 的元素和
preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1];
// 计算子矩阵 [x1, y1, x2, y2] 的元素和
public int sumRegion(int x1, int y1, int x2, int y2)
// 目标矩阵之和由四个相邻矩阵运算获得
return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1];
/**
* Your NumMatrix object will be instantiated and called as such:
* NumMatrix obj = new NumMatrix(matrix);
* int param_1 = obj.sumRegion(row1,col1,row2,col2);
*/
差分数组
差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。
构造差分数组
先对 nums
数组构造一个 diff
差分数组,diff[i]
就是 nums[i]
和 nums[i-1]
之差:
int[] diff = new int[nums.length];
// 构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++)
diff[i] = nums[i] - nums[i - 1];
通过这个 diff
差分数组是可以反推出原始数组 nums
的
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++)
res[i] = res[i - 1] + diff[i];
这样构造差分数组 diff
,就可以快速进行区间增减的操作,如果你想对区间 nums[i..j]
的元素全部加 3,那么只需要让 diff[i] += 3
,然后再让 diff[j+1] -= 3
即可:
最终的差分工具类
// 差分数组工具类
class Difference
// 差分数组
private int[] diff;
/* 输入一个初始数组,区间操作将在这个数组上进行 */
public Difference(int[] nums)
assert nums.length > 0;
diff = new int[nums.length];
// 根据初始数组构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++)
diff[i] = nums[i] - nums[i - 1];
/* 给闭区间 [i, j] 增加 val(可以是负数)*/
public void increment(int i, int j, int val)
diff[i] += val;
if (j + 1 < diff.length)
diff[j + 1] -= val;
/* 返回结果数组 */
public int[] result()
int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++)
res[i] = res[i - 1] + diff[i];
学习算法和刷题的框架思维
一句话总结:从整体到细节,自顶向下,从抽象到具体的框架思维是通用的。
数据结构的存储方式:数组和链表才是结构基础,其他的数据结构都是在它们上的特殊操作。
二者的优缺点:
数据结构的基本操作:遍历+访问,访问即增删查改。
数据结构种类很多,它们存在的目的是更高效地增删查改。
访问分为线性的和非线性的。线性:for/while;非线性:递归。
遍历框架:
//数组
void traverse(int []arr)
{
for(int i=0;i<arr.length;i++)
{
...
}
}
//链表
void traverse(ListNode head)
{
for(ListNode p=head;p!=null;p=p.next)
{
...
}
}
//非线性:递归
void traverse(ListNode head)
{
traverse(head.next);
}
算法刷题指南
先刷二叉树。因为它最容易培养框架思维。
只要涉及递归的问题,基本都是树的问题。
动态规划详解
一般形式是求最值。
核心问题是穷举,但因为存在重叠子问题,穷举是可以优化的。
一定会具备最优子结构,通过子问题得到原问题的答案。
最难的是写出正确的状态转移方程
一个帮助思考状态转移方程的思维框架:
明确状态——dp数组/函数的含义——明确选择——明确base case