leetcode--数组

本文详细介绍了二分查找、双指针等算法在数组问题中的应用,如搜索插入位置、查找元素范围、移除元素、移动排序数组中的重复项等。通过实例解析,展示了如何利用这些算法高效地解决问题,并提供了相应的代码实现,有助于提升算法理解和编程能力。
摘要由CSDN通过智能技术生成

目录

1.二分查找

2.双指针

3.滑动窗口


1.二分查找

二分法的条件:有序数组、没有重复元素

二分法重点

①注意区间

左闭右闭区间:[left,right]  可以取到left == right 所以循环写成while(left<=right) right = length - 1
左闭右开区间:[left,right)  不可以取到left == right 所以循环写成while(left<right) right = length

②注意middle取值:为了防止溢出 middle = left + (right - left) / 2,因为left + right 可能会溢出

704 二分查找

第一种写法

int search(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size() - 1;  //target 定义在[left,right]区间内
    while(left <= right){
        int middle = left + (right - left) / 2;
        if(nums[middle] > target )
        {
            right = middle -1;
        }else if(nums[middle] < target){
            left = middle + 1;
        }else{
            return middle;
        }
    }
    return -1;
}
时间复杂度:O(log n),其中 n 是数组的长度。

第二种写法

int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
            int middle = left + ((right - left) / 2);
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在[middle + 1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值,直接返回下标
            }
        }
        // 未找到目标值
        return -1;
    }

35 搜索插入位置

int searchInsert(vector<int> &nums, int target){
    int left = 0;
    int right = nums.size() - 1;
    while(left <= right){
        int middle = left + (right - left) / 2;
        if(nums[middle] > target){
            right = middle - 1;
        }else if(nums[middle] < target){
            left = middle + 1;
        }else{
            return middle;
        }
    }
    return right + 1;
}

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

target 有三种情况:

1.target 在数组的最左边或最右边 返回[-1,-1]

2.target 在数组范围内,但是数组中不存在target 返回[-1,-1]

3.target 在数组范围内,且数组中有target 返回对应的下标

 //找左边界  对于[3,3]数组如果找小于等于3的数 返回-1;找大于3的数 返回-2
 int getLeftBorder(vector<int> nums, int target){
        int left = 0;
        int right = nums.size() - 1;
        int leftborder = -2;
        while(left <= right){
            int middle = left + (right - left) / 2;
            if(nums[middle] >= target){
                right = middle - 1;
                leftborder = right;
            }else{  
                left = middle + 1;
            }
        }
        return leftborder; 
 }
 //找右边界  对于[3,3]数组如果找大于3的数(包括3) 返回2;找小于3的数 返回-2
 int getRightBorder(vector<int> nums, int target){
        int left = 0;
        int right = nums.size() - 1;
        int rightborder = -2;
        while(left <= right){
            int middle = left + (right - left) / 2;
            if(nums[middle] > target){
                right = middle - 1;
            }else{
                left = middle + 1;
                rightborder = left;
            }  
        }
        return rightborder; 
    }
vector<int> searchRange(vector<int>& nums, int target) {
        int leftborder = getLeftBorder(nums,target);
        int rightborder = getRightBorder(nums,target);
        //情况一 target在左右边界  只要有一个是-2 意味着在边界
        if(leftborder == -2 || rightborder == -2)  return {-1,-1};
        //情况三
        if(rightborder - leftborder > 1) return {leftborder + 1, rightborder - 1};
        //情况二
        return {-1,-1};
}

69 x的平方根

思路:求平方根的时候也可以使用二分查找,left = 0 right = x,只需要判断mid平方和x的关系就能找到x的平方根,这个题要求返回的是整数部分,小数部分舍去

    int mySqrt(int x) {
        int left = 0,right = x;
        while(left <= right){
            int middle = left + (right - left)/2;
            if( (long)middle * middle <= x){
                left = middle + 1;
            }else{
                right = middle - 1;
            }
        }
        return right;
    }
   //时间复杂度O(log  x)
   //空间复杂度O(1)

367 有效的完全平方数

思路和上面的题类似,使用二分查找,在if判断的时候不一样,分为三种情况:mid平方刚好等于num返回true;小于就left = mid + 1;大于right = mid -1

    bool isPerfectSquare(int num) {
        int left = 0, right = num;
        while(left <= right){
            int middle = left + (right - left) / 2;
            if((long)middle * middle == num){
                return true;
            }else if((long)middle * middle < num){
                left = middle + 1;
            }else{
                right = middle - 1;
            }
        }
        return false;
    }

2.双指针

27 移除元素

移除数组中等于val值的元素,空出位置由下一个元素补上。

不能直接删除该val的原因:数组的内存地址是连续的,只能覆盖元素的值。

思路:双指针法,left和right指针开始都指向数组起始位置,如果当right指针指向的值不是要删除的元素,用left指针记录该值,然后left++;如果right指向的是要删除的元素,left指针什么都不执行,right指向下一个值,最后left的索引值就是移除元素数组的长度   (left++)

//27 移除元素
int removeElement(vector<int>& nums, int val) {
    int left = 0;
    for(int right = 0; right < nums.size(); right++){
        if(nums[right] != val){
            nums[left] = nums[right];
            left++;
            //上面两行代码简化成nums[left++] = nums[right];
        }
    }
    return left;
}
时间O(n)
空间O(1)

26 移动排序数组中的重复项

思路:首先判断数组大小,如果为0,直接返回0;如果数组长度大于0,从数组索引值1的位置开始使用双指针,判断方法和上一个代码一样

int removeDuplicates(vector<int>& nums) {
    int n = nums.size();
    if(n == 0){
        return 0;
    }
    int left = 1,right = 1;
    for(;right < n;right++){
        if(nums[right] != nums[right - 1]){
            nums[left] = nums[right];
            left++;
        } 
    }
    return left;
}
O(n)
O(1)

283 移动零

void swap(vector<int>& nums,int left,int right){
        int temp = nums[left];
        nums[left] = nums[right];
        nums[right] = temp;
}
void moveZeroes(vector<int>& nums) {
        int left = 0, right = 0;
        for(;right <  nums.size(); right++){
            if(nums[right] != 0){
                swap(nums,left,right); //left和right相同交换操作相当于没做,如果不相同说明当前left指向的元素是0
                left++;
            }
        }
}

844 比较含退格的字符串

思路:使用双指针法,从后向前遍历字符串,定义snums是字符串s中‘#’的数量,同时也是要删除字符的数量。开始遍历字符串:

  • 当遍历到 ‘#’, snums++;
  • 当遍历到普通字符,判断要删除字符的数量,当snums大于0,当前字符要删掉,然后snums-1,如果snums 等于0,说明当前字符不需要删掉,此时没有需要删除的字符,退出循环。(注意退出循环的索引值)

 举个例子:对于”abc#“,执行到b位置时循环就已经退出了,此时的索引值是1;对于”a#b#“循环会一直执行直到索引值是-1为止;对于'a#bc',直接退出找#数量的循环,索引值是3

这个题代码看不懂可以举几个例子debug,看看i和j是如何变化的

 bool backspaceCompare(string s, string t) {
        int snums = 0, tnums = 0;
        int i = s.length() -1;
        int j = t.length() -1;
        while(1){
            for(; i >= 0; i--){
                if(s[i] == '#') snums++;  
                else{
                    if(snums > 0)  snums--;   //大于0表示当前字符要删除,
                    else break;               //表示当前字符不要删除,退出循环
                }
            }
            for(;j >= 0; j--){
                if(t[j] == '#') tnums++;  
                else{
                    if(tnums > 0)  tnums--;   //大于0表示当前字符要删除,
                    else break;               //表示当前字符不要删除,本次循环
                }
            }
            //对于字符串‘a#b#’ i=-1 字符串‘abc#’ j=1
            //两次字符都判断完后比较s[i]和t[i]
            if(i < 0 || j < 0) break;  //当一个字符串已经结束了所有遍历,退出循环
            if(s[i] != t[j])  return false; 
            i--;j--;
        }
        if(i == -1 && j == -1) return true;
        return false;
    }
时间复杂度O(n+m)
空间复杂度O(1)

除了双指针外可以使用栈解决这个问题

思路:定义一个字符串a作为栈,遍历s字符串,遇到字符将其放到a中,遇到 '#' 则从a中删除一个元素

bool backspaceCompare1(string s, string t) {
    string a;//当作栈使用
    string b;//当作栈使用
    for(int i = 0; i < s.length(); i++){
        if(s[i] != '#')  
            a = a + s[i];
        else if(!s.empty())   
            a.pop_back();
    }
    for(int i = 0; i < t.length(); i++){
        if(t[i] != '#')  
            b = b + t[i];
        else if(!t.empty())   
            b.pop_back();
    }
    if( a == b) 
        return true;
    else 
        return false;
}
时间复杂度O(n+m)
空间复杂度O(n+m)

977有序数组的平方

思路:把每个数的平方放到数组中,对这个数组进行排序,时间复杂度是O(n+nlongn) = O(nlogn),也可以使用双指针法,因为数组中可能由负数,平方的最大值是数组最左边或者最右边的数,因此left指向起始位置,right指向结束位置,将最大的平方值放到新数组的末尾,新数组和原数组长度一样。

    vector<int> sortedSquares(vector<int>& nums) {
        int k = nums.size() - 1;
        vector<int> newnums(nums.size(),0);  //每个元素为0

        int left = 0, right = nums.size()- 1;
        while(left <= right){
            if(nums[left] * nums[left] <= nums[right] * nums[right]){
                newnums[k] = nums[right] * nums[right];
                k--;
                right--;
            }else {  //[left]^2 > [right]^2
                newnums[k] = nums[left] * nums[left];
                k--;
                left++;
            }
        }
        return newnums;
    }
时间复杂度O(n)
空间复杂度O(1)

3.滑动窗口

209 长度最小的子数组

思路:第一种使用两个for循环遍历,找符合要求的子数组。

int minSubArrayLen(vector<int> nums,int s){
    //初始化子数字的最小长度是无穷大 INT_MAX
    int length = INT_MAX;
    for(int i = 0; i < nums.size(); i++){
        int sum = 0;
        for(int j = i; j < nums.size(); j++){
            sum += nums[j];
            if(sum >= s){
                length = min(length, j - i + 1);
                break;
            }
        }
    }
    return length == INT_MAX ? 0 : length;
}

思路:暴力法复杂度是O(n^2),也可以使用滑动窗口法,不断调节子序列的起始位置和终止位置。i和j开始都指向0,j往后移动,只要当 当前窗口的值大于s,窗口需要向后移动,也就是i++,此时窗口内元素之和是sum - nums[i].

int minSubArrayLen(int target, vector<int>& nums) {
    int length = INT_MAX;  //返回子数组的长度
    int sum = 0;
    int i = 0,j = 0;
    while(j < nums.size()){
        sum += nums[j];
        while(sum >= target){ //当前窗口满足s
            length = min(length, j - i + 1);
            sum = sum - nums[i];
            i++;
        }
        j++;
    }
    return length == INT_MAX ? 0 : length;
 }

904 水果成篮

思路:这个题理解为 含两种元素的最大连续子序列

right向右移动,如果fruits[right]指向的是两种水果的一种,计算当前长度,然后right右移;如果fruits[right]不是两种水果中的一个将left移动到right前一个位置,这里left需要往前找是否有相同的数,条件是left大于等于一(如果left等于0就不需要往前找)同时left-1和left指向的数相同,满足条件left-1继续找,找到最前面的left后计算此时的长度

int totalFruit(vector<int>& fruits) {
        int left = 0, right = 0;
        int length = 0;
        int a = fruits[left], b = fruits[right];
        while(right < fruits.size()){
            if(fruits[right] == a || fruits[right] == b){   //right指向的满足两种水果中的一种
                length = max(length , right - left + 1);
                right++;
            }else{ //不满足,left = right - 1 同时left也需要往前找是否有相同的
                left = right - 1;
                a = fruits[left];
                while(left >= 1 && fruits[left-1] == a){  //left往前找,如果此时left已经是0就没必要往前,所以left大于1 同时判断left-1 是否和left 指向的数相同 
                    left--;  //满足条件再次继续往前找
                }
                //left已经找到最前面
                b = fruits[right];
                length = max(length , right - left + 1);
            }
        }
        return length;
    }

总结:209 长度最小的子数组 904 水果成篮 分别是求满足条件的最小子数组和最大子数组。

对于最小数组:L和R指向起始位置,R向右移动,如果窗内元素满足条件,L向右缩小窗口,计算最优结果;如果窗内元素不满足条件,R继续向右扩大。

while ( R < s.size()) {
    窗口右端扩展,加进s[R], 更新条件
    while(满足条件) {
        比较最小长度
        窗口左端移除s[L],更新条件,L++
    }
    R++;
}

对于最大数组模板:L和R指向起始位置,R向右移动,如果窗内元素满足条件,R向右移动扩大窗口,更新结果;如果窗口内元素不满足条件,L向右缩小窗口。

while ( R < s.size()) {
	窗口右端扩展,加进s[R], 更新条件
	while(不满足条件) {
		窗口左端移除s[L],更新条件,然后L++
	}
	比较最大长度
    R++;
}

对于子串问题也可以使用滑动窗口问题,有大佬总结过子串问题的模板

/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
     //need记录的是t字符出现次数,window记录当前窗口字符出现次数
    unordered_map<char, int> need, window;  
    for (char c : t) need[c]++;    
    
    int left = 0, right = 0;
    //valid 表示窗口内满足need 条件的字符个数,如果valid = need.size 说明当前窗口完全覆盖了t字符串
    int valid = 0;   
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            //如果是求最小子串,在这里更新最终结果

            // d 是将移出窗口的字符
            char d = s[left];
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
         //如果是求最大子串,在这里更新最终结果
    }
    return
}

两处... 是更新窗口数据的地方,使用模板需要注意四个问题:

1.移动right扩大窗口时需要更新哪些数据?

2.什么时候开始缩小窗口(移动left)?

3.移动left时哪些数据需要更新?

4.需要的结果在扩大窗口还是缩小窗口时更新?  对于求最小类问题,在缩小窗口更新最终结果

76 最小覆盖子串     相当于求满足条件的最小子串

思路:①当字符进入窗口时,增加window计数器;②当valid = need.size时缩小窗口;③字符移除窗口减少window计数器;④缩小窗口时更新最终结果

string minWindow(string s, string t) {
        unordered_map<char,int>need, window;  //need是t字符出现次数
        for(char a : t)
            need[a]++;

        int left = 0, right = 0;
        int valid = 0; //valid 表示窗口内满足need 条件的字符个数,如果valid = need.size 说明当前窗口完全覆盖了t字符串,然后缩小敞口
        //记录最小字符串的起始索引和长度
        int start = 0, len = INT_MAX;
        while(right < s.length()){
            char a = s[right];  //a是进入窗口的字符
            right++;
            //窗口内更新数据
            if(need.count(a)){  //count 容器内key返回1,否则返回0
                window[a]++;
                if(window[a] == need[a]) //对于同一个字符两个容器记录的次数相同
                    valid++;
            }
            //窗口收缩
            while(valid == need.size()){
                //更新最终结果
                if(right - left < len){
                    start = left;
                    len = right - left;
                }
                //判断是否有字符需要移除
                char b = s[left];
                left++;
                //更新窗口数据
                if(need.count(b)){
                    if(window[b] == need[b])  //某一字符出现次数相同
                        valid--;  //退出缩小窗口
                    window[b]--;
                }
            }         
        }
        return len == INT_MAX ? "" : s.substr(start,len);
    }

3 最长无重复子串     相当于求满足条件的最大子串

int lengthOfLongestSubstring2(string s) {
    unordered_map<char,int>window;
    int left = 0, right = 0;
    int length = 0;
    while(right < s.length()){
        char ref = s[right];
        right++;
        //更新窗口数据
        window[ref]++;
        //收缩窗口
        while(window[ref] > 1){
            char cur = s[left];
            left++;
            //更新窗口
            window[cur]--;
        }
        //更新最终结果
        length = max(length,right - left);
    }
    return length;
}

4.螺旋矩阵

54 螺旋矩阵

 vector<int> spiralOrder(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n =matrix[0].size();
        vector<int>ans;
        int up = 0, down = m - 1, left = 0, right = n - 1;
        while(1){
            //上边界开始 从左向右遍历,遍历完上边界下移,如果上边界移动比下边界还要大,退出循环
            for(int i = left; i <= right; i++) ans.push_back(matrix[up][i]);
            if(++up > down) break;

            //右边界,从上往下遍历,遍历完右边界左移
            for(int i = up; i <= down; i++) ans.push_back(matrix[i][right]);
            if(--right < left) break;

            //下边界,从右往左遍历,遍历完下边界上移
            for(int i = right; i >= left; i--) ans.push_back(matrix[down][i]);
            if(--down < up) break;

            //左边界,从下往上遍历,遍历完左边界右移
            for(int i = down; i >= up; i--) ans.push_back(matrix[i][left]);
            if(++left > right) break;
        }
    return ans;
 }

59 螺旋矩阵Ⅱ

vector<vector<int>> generateMatrix(int n) {
       int up = 0, down = n - 1, left = 0, right = n - 1;
        int num = 1;
        vector<vector<int>> ans(n,vector<int>(n));
        while(1){
            //上边界开始 从左向右遍历,遍历完上边界下移,如果上边界移动比下边界还要大,退出循环
            for(int i = left; i <= right; i++) ans[up][i] = num++;
            if(++up > down) break;

            //右边界,从上往下遍历,遍历完右边界左移
            for(int i = up; i <= down; i++) ans[i][right] = num++;
            if(--right < left) break;

            //下边界,从右往左遍历,遍历完下边界上移
            for(int i = right; i >= left; i--)  ans[down][i] = num++;
            if(--down < up) break;

            //左边界,从下往上遍历,遍历完左边界右移 
            for(int i = down; i >= up; i--)  ans[i][left] = num++;
            if(++left > right ) break;
        }
        return ans;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值