[仅个人学习使用]
摘抄代码随想录中的算法总结 从leetcode中截取部分为数组一类的算法题合集 此部分为数组系列
包括动态窗口 双指针加哈希表 二分查找模板 共九道题
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
代码案例1:输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
代码案例2:输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
注:你可以假设 nums 中的所有元素是不重复的。
n 将在 [1, 10000]之间。
nums 的每个元素都将在 [-9999, 9999]之间。
这个题就是二分模板的一个模板题
想看详情可以点击这里 !!!!数的范围-二分算法-java实现!!!!!
class Solution {
public int search(int[] nums, int target) {
int l = 0 ;
int r = nums.length - 1;
while(l < r){
int mid = (l+r) >> 1 ;
if(nums[mid] >= target) r = mid ;
else l = mid + 1 ;
}
if(nums[l] != target) return -1 ;
return l ;//这里面写l 和r都可以
}
}
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
输入: nums = [1,3,5,6], target = 5
输出: 2
输入: nums = [1,3,5,6], target = 2
输出: 1
输入: nums = [1,3,5,6], target = 7
输出: 4
找到第一个大于等于target的位置 如果所有的数都小于target 那么应该插入到最后 所以l和r的范围 应该是0 - Nums.legnth
class Solution {
public int searchInsert(int[] nums, int target) {
int l = 0,r = nums.length ;
while(l < r)
{
int mid = l + r >> 1;
if(nums[mid] >= target) r = mid ;
else l = mid + 1;
}
return l;
}
}
这样改边界值这种的话 需要特判一下
class Solution {
public int searchInsert(int[] nums, int target) {
int l = 0, r = nums.length - 1;
while (l < r) {
int mid = l + r >> 1;
if (nums[mid] >= target) {
r = mid;
} else {
l = mid + 1;
}
}
if (nums[l] < target) {
return l + 1;
}
return l;
}
}
这种解法是通过二分找到 小于target的 最大的元素的位置,返回该位置的下一个位置
class Solution {
public int searchInsert(int[] nums, int target) {
int n = nums.length;
int l = -1,r = n - 1;
while(l < r)
{
int mid = l + r + 1 >> 1;
if(nums[mid] < target) l = mid ;
else r = mid - 1;
}
return l + 1;
}
}
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
相等的就减掉
class Solution {
public int removeElement(int[] nums, int val) {
int k = 0 ;//返回新数组的长度
for(int i = 0 ; i < nums.length ; i++){
if(nums[i] != val){
nums[k++] = nums[i];
}
}
return k;
}
}
也可以按照相等的来 但是这样显然没有直接按照不等的好
class Solution {
public int removeElement(int[] nums, int val) {
int j = nums.length - 1;
for (int i = 0; i <= j; i++) {
if (nums[i] == val) {
swap(nums, i--, j--);
}
}
return j + 1;
}
void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
注意:
蠢办法的话
这样的话是nlogn
class Solution {
public int[] sortedSquares(int[] nums) {
int k = 0 ;
for(int i = 0 ; i < nums.length ; i++){
nums[k++] = nums[i]* nums[i] ;
}
Arrays.sort(nums);
return nums;
}
}
可以按照归并的方法来 就是类似归并排序的算法
我们可以使用两个指针分别指向位置 0 和n−1,每次比较两个指针对应的数,选择较大的那个逆序放入答案并移动指针。这种方法无需处理某一指针移动至边界的情况,
最主要的是研究负数平方后 数组的顺序就需要重新排序 而最大的和最小的只能出现在数组的两边 而无法用k=0开始 因为平方后最小的数没在两边 在数组的中间位置
class Solution {
public int[] sortedSquares(int[] nums) {
int n = nums.length;
int[] res = new int[n];
for(int i = 0 , j = n -1 , k = n-1 ; i <= j ;){
if( nums[i]* nums[i] > nums[j]* nums[j]){
res[k] = nums[i]* nums[i]; k--;i++;
} else {
res[k] = nums[j]* nums[j];k--;j--;
}
}
return res;
}
}
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例1
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例2
输入:target = 4, nums = [1,4,4]
输出:1
示例3:输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
注意:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 105
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int ans = Integer.MAX_VALUE;
for(int i = 0 , j = 0 , sum = 0 ; i < nums.length ; i++){
sum += nums[i] ;
//while循环就是相互去试
while(sum - nums[j] >= target) sum -= nums[j++];
if(sum >= target) ans = Math.min(ans , i - j +1);
}
if(ans == Integer.MAX_VALUE) ans = 0 ;
return ans ;
}
}
改进 改成前缀和 不过前缀和的话 空间复杂度会多出一个O(n)
前缀和模板
for (int i = 1; i <= n; i ++ ){
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int n = nums.length;
int[] f = new int[n + 1];
for(int i = 1;i <= n;i ++) f[i] = f[i - 1] + nums[i - 1];
int ans = Integer.MAX_VALUE;
for(int j = 1, i = 0;j <= n;j ++)
{
while(f[j] - f[i] >= s)
{
ans = Math.min(ans, j - i);
i ++;
}
}
if(ans == Integer.MAX_VALUE) ans = 0 ;
return ans ;
}
}
1、计算出数字的前缀和f[],即找到f[j] - f[i] >= s 的最小连续长度j - i
2、枚举i位置,固定i位置,在后面的元素中找出f[j] - f[i] >= s 最小的j位置,则对应的长度j - i就是当前位置i的最小连续长度
3、由于f[]数组是单调的,因此在区间[i + 1, n]找最小的f[j],由于可能会存在[i + 1, n]区间中都不存在一个数j,使得f[j] - f[i] >= s,为了方便区分存不存在,因此左右边界开始设置为[i, n + 1],当最后落在i或者n + 1时,则表示不存在f[j]这个数
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int n = nums.length;
int[] f = new int[n + 1];
for(int i = 1;i <= n;i ++) f[i] = f[i - 1] + nums[i - 1];
int ans = Integer.MAX_VALUE;
for(int i = 0;i < n;i ++){
int l = i, r = n + 1;
while(l < r){
int mid = l + r >> 1;
if(f[mid] - f[i] >= s) r = mid;
else l = mid + 1;
}
if(r >= i + 1 && r <= n) ans = Math.min(ans, r - i);
}
if(ans == Integer.MAX_VALUE) return 0;
return ans;
}
}
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。
注意:
示例1 :
输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
示例2:输入:s = “a”, t = “a”
输出:“a”
示例3:输入: s = “a”, t = “aa”
输出: “”
解释: t 中两个字符 ‘a’ 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
1 <= s.length, t.length <= 105
s 和 t 由英文字母组成
进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗?
双指针算法的 i j 条件就是前面的j 往后走 i 也往后走 单调才可以使用双指针算法 或者叫滑动窗口算法
1、cnt维护的是s字符串[j,i]区间中满足t字符串的元素的个数(此题的主要精髓)
2、hs哈希表维护的是[j,i]区间中各个字符出现多少次,ht哈希表维护的是t字符串各个字符出现多少次
3、枚举过程中,将s[i]加入到hs中,同时观察s[j]是否多余,若多余则移出
4、将s[i]加入到hs中,若hs[i] <= ht[i],表示多了一个符号的元素,cnt ++
5、若当前双指针维护的窗口满足个数要求,则更新答案
java写起来确实很费力 注意的是边界问题 t可能为0 的情况
// 有效字符数cnt == t.size() 后 res 赋值的情况有两种:
// 1. res为空时, 是 第 1 次赋值
// 2. res.size() 变小后, 是 第 2, 3, 4, 5....次赋值
if (cnt == t.size())
if (res.empty() || i - j + 1 < res.size())
res = s.substr(j, i - j + 1);
简洁版
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> hs, ht;
for (auto c: t) ht[c] ++ ;
string res;
int cnt = 0;
for (int i = 0, j = 0; i < s.size(); i ++ ) {
hs[s[i]] ++ ;
if (hs[s[i]] <= ht[s[i]]) cnt ++ ;
while (hs[s[j]] > ht[s[j]]) hs[s[j ++ ]] -- ;
if (cnt == t.size()) {
if (res.empty() || i - j + 1 < res.size())
res = s.substr(j, i - j + 1);
}
}
return res;
}
};
注释版
// 双指针 滑动窗口 要满足单调性。即 i, j 指针 均只能 朝着 一个方向移动, 不能朝一个方向移动 再 向另一个方向移动.
// 设滑动窗口 s[j ~ i], 设 指针 i 为 滑动窗口的 终点, 指针 j 为 滑动窗口的 起点.
// 设 哈希表 hw(hash_window) 存的是 滑动窗口内 各字符的数量, ht存的是 字符串t内各字符数量.
//
// 问题 1 :如何 快速判断 滑动窗口 s[j ~ i] 内 是否 已经包含了 t内所有字符了呢(每种字符可能重复)?
// 滑动窗口 内的 有效字符 总数量 cnt = t.size(), 就说明已经包含了 t 内所有字符.
// 问题 2 :那么 什么是 有效字符呢?
// 如果 此时 新加入了 s[i]字符之后, hw[s[i]] <= ht[s[i]], 此时的字符 s[i]有用, 就算有效字符, cnt ++.
// 如果 新加入了 s[i]字符之后, 有 hw[s[i]] > ht[s[i]] 了, 说明加入当前字符s[i] 之前, 滑动窗口内 s[i]的数量
// --- 就已经 达到了 t内这个字符s[i]的数量, 原本关于 字符s[i] 就已经达标, 现在再加就是冗余, 就是无效字符.
//
// 问题 3 :滑动窗口 s[j ~ i]的 起点 指针 j 什么时候 才 向右 移动?
// 如果 s[j]处的字符 (字符记为#) 的数量 大于 t 中 这个字符 # 的数量, 即 hw[s[j]] > ht[s[j]],
// --- j 就可以向右移动, 因为 s[j] 处的字符 # 的数量 冗余了, j 向右移动的时候, 同时 将 hw[s[j]] --
// 问题 4 : 有效字符的数量 cnt 变化是靠的什么呢?
// cnt ++ 只能通过 指针 i 向右移动, 但是 注意 指针 i 不一定 每次 向右移动 都会 使有效字符 cnt ++。
// 指针 j 向右移动 不会影响 有效字符cnt 的数量, 即 虽然不会 cnt ++, 但是也不会出现 导致cnt --.
//
// 问题 5 : 指针 i 和 指针 j 的作用, 这里 打个 形象的比喻 ~~~
// 指针 i 的作用是 开源,负责 增加有效字符cnt 的数量, 开疆破土, 助力 有效字符数量 cnt 早日达到 t.size();
// 指针 j 的作用是 节流,负责 将 指针 i 开辟的疆土 中 无价值 的部分 即 冗余的字符 回收,达到 最小 覆盖子串的目的。
//
// 问题 6 :双指针工作的流程 是 怎样的呢? 明白了 指针 j 和 i 的作用 就很好理解啦 ~
// (1) 肯定是 指针 i 先移动, hw[s[i]] ++, 然后看看 是否 会 增加 cnt, 通过 if (hw[s[i]] <= ht[s[i]]) 来判断
// (2) i 每次向右移动一步之后, 每次 i 移动之后 都可能会导致 cnt == t.size(), 因为题目是求 最小 覆盖子串, 所以
// --- 在 判断 if (cnt == t.size()) 之前 要 先 用 j 将 无用的 字符回收, 达到 最小覆盖子串 的目的
// --- j 就负责 看看 i 之前开辟的疆土里面有没有 冗余的字符, 有的话就回收, hw[s[j]] -- 同时 j ++
// (3) i 每次开辟完新的疆土 同时 j 也回收了 可能存在的 无价值的 冗余字符, 就可以判断 cnt == t.size()啦 ~~
class Solution {
public:
string minWindow(string s, string t) {
// hw 存的是 滑动窗口内 各字符数量, ht 存的是 字符串t 的 各字符数量
unordered_map<char, int> hw, ht; // 也可开一个哈希表作差,但是开两个思路更清晰
for (auto c: t) ht[c] ++ ; // 统计 字符串 t 中 各字符 数量
string res;
int cnt = 0; // 注意: cnt 中 存的是 有效字符的数量
for (int i = 0, j = 0; i < s.size(); i ++ ) {
// (1) 指针 i 先移动, hw[s[i]] ++, 然后看看 是否 会 增加 cnt, 通过 if (hw[s[i]] <= ht[s[i]]) 来判断
hw[s[i]] ++ ;
if (hw[s[i]] <= ht[s[i]]) cnt ++ ;
// (2) j 就负责 看看 i 之前开辟的疆土里面有没有 冗余的字符, 有的话就回收, hw[s[j]] -- 同时 j ++
// 每次 i 移动完 之后, 判断 j 能不能向右移动, 注意 j 可以连续移动, 用while 不是 if
// while (hw[s[j]] > ht[s[j]]) hw[s[j ++ ]] -- ; // 是 while, 不是 if
while (j <= i && hw[s[j]] > ht[s[j]]) hw[s[j ++ ]] -- ; // 加上 j <= i,防止字符串下标越界
// (3) i 每次开辟完新的疆土 同时 j 也回收了 可能存在的 无价值的 冗余字符, 就可以判断 cnt == t.size()啦
if (cnt == t.size()) { // 更新答案
if (res.empty() || i - j + 1 < res.size()) // 注意 res.empty()
res = s.substr(j, i - j + 1);
}
}
return res;
}
};
class Solution {
public String minWindow(String s, String t) {
HashMap<Character,Integer> hs = new HashMap<Character,Integer>();//存s字符串的哈希表 滑动窗口
HashMap<Character,Integer> ht = new HashMap<Character,Integer>();//存t字符串的哈希表
for(char a : t.toCharArray() ) {//向哈希表ht中计数
ht.put(a,ht.getOrDefault(a,0)+1);
}
String res = "" ;
int cnt = 0 ;
for(int i = 0, j = 0 ; i < s.length() ; i++){
hs.put(s.charAt(i),hs.getOrDefault(s.charAt(i),0)+1);//向哈希表hs中计数
//滑动窗口向右移动的时候 多加入一个字符 使得hs小与等于ht里面的字符个数 这样的叫做有效字符
if( hs.get(s.charAt(i)) <= ht.getOrDefault(s.charAt(i),0)) cnt ++;//有效计数加一
// if( ht.containsKey(s.charAt(i)) && hs.get(s.charAt(i)) <= ht.get(s.charAt(i))) cnt ++;
//开始滑动窗口左边的j移动 反过来的话 滑动窗口就需要移动了 记得此时是j向右移动
while( j < i && hs.get(s.charAt(j)) > ht.getOrDefault(s.charAt(j),0)) {
//加上 j <= i,防止字符串下标越界
hs.put( s.charAt(j) ,hs.get(s.charAt(j))-1);j++;
}
if(cnt == t.length()){
if(res.isEmpty() || i - j + 1 < res.length() )
res = s.substring(j , i + 1);
}
}
return res ;
}
}
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。
你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:
你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。
输入:fruits = [1,2,1]
输出:3
解释:可以采摘全部 3 棵树。
输入:fruits = [0,1,2,2]
输出:3
解释:可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。
输入:fruits = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。
这题就是在寻找 序列中长度最长的 只包含两个元素的 区间的长度
class Solution {
public int totalFruit(int[] fruits) {
int res =0 ;
HashMap<Integer,Integer> cnt = new HashMap<>();
//s表示哈希表中有多少个不同元素
for(int i =0 , j = 0 , s = 0 ; i < fruits.length ; i++){
cnt.put(fruits[i],cnt.getOrDefault(fruits[i],0)+1);//先放哈希表中
if(cnt.get(fruits[i]) == 1 )s++;//说明当前元素是一个新的元素 种类加一
while(s > 2){
//此时种类要是超于2 j就往后走 所以s就会被减去
cnt.put(fruits[j],cnt.get(fruits[j])-1);
if(cnt.get(fruits[j]) == 0 ) s--;
j++;
}
res = Math.max(res,i - j + 1);
}
return res ;
}
}
从左上角开始遍历,先往右走,走到不能走为止,然后更改到下个方向,再走到不能走为止,依次类推,遍历 n2n2 个格子后停止。
时间复杂度分析:矩阵中每个格子遍历一次,所以总时间复杂度是 O(n2) 。
class Solution {
public int[][] generateMatrix(int n) {
int[][] ans = new int[n][n];
int dx[] = {0, 1, 0, -1};//右下左上
int dy[] = {1, 0, -1, 0};
for (int x = 0, y = 0, i = 1, d = 0; i <= n * n; i ++ )//d表示方向
{
ans[x][y] = i ;
int a = x + dx[d], b = y + dy[d];
if (a < 0 || a >= n || b < 0 || b >= n || ans[a][b] != 0)//越界 或者说这个格子已经填数了
{
d = (d + 1) % 4;
a = x + dx[d]; b = y + dy[d];
}
x = a; y = b;
}
return ans;
}
}
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
一道经典的双指针算法的题
class Solution {
public int lengthOfLongestSubstring(String s) {
HashMap<Character,Integer> map = new HashMap<>();
if(s.length() == 0) return 0;
int ans = 0 ;
for(int i = 0 ,j = 0 ; i < s.length() ; i++){
map.put(s.charAt(i),map.getOrDefault(s.charAt(i),0)+1); //把s放入哈希表中
while(map.get(s.charAt(i)) > 1){//判断是否有重复元素 关于j++的都是循环起步
//对哈希表进行操作
map.put(s.charAt(j),map.get(s.charAt(j))-1);
j++;
}
ans = Math.max(ans,i - j + 1);
}
return ans ;
}
}