数据结构与算法(一)

目录

  • 第一章:数组
    • 【二分查找法】
    • leetcode704:二分查找
    • 剑指Offer53_II
    • leetcode1011:在D天内送达包裹的能力
    • leetcode1482:制作m束花所需的最少天数(与leetcode1011:解题思路相同)
    • leetcode1733:完成所有工作的最短时间
    • leecode69:x的平方根
    • leetcode29:两数相除
    • leetcode33:搜索旋转排序数组
    • leetcode81:搜索旋转排序数组II
    • leetcode34:在排序数组中查找元素的第一个和最后一个位置
    • leetcode:35:搜索插入位置
    • leetcode153:寻找旋转排序数组中的最小值
    • leetcode154 寻找旋转排序数组中的最小值II
    • 【双指针】
    • leetcode283:移动零
    • leetcode26:删除排序数组中的重复项
    • leetcode27:移除元素
    • leetcode80:删除排序数组中重复项II(做错很多次)
    • leetcode844:比较含退格的字符串
    • leetcode633:平方数之和
    • 面试题17.21:直方图水量(接雨水)
    • leetcode167:两数之和II(与平方数之和解法相同就是不一样的描述)
    • leetcode151:反转单词顺序
    • leetcode125:验证回文串
    • leetcode344:反转字符串
    • leetcode345:反转字符串中的元音字母
    • leetcode11:盛最多水的容器
    • leetcode977:有序数组的平方(方法二:归并排序值得学习还有与之对应的分而治之)
    • leetcode844:比较含退格的字符串
    • 滑动窗口
    • leetcode209:长度最小的子数组
    • leetcode3:无重复字符的最长子串
    • leetcode76:最小覆盖子串
    • leetcode424:替换后的最长重复字符
    • leetcode438:找到字符串中所有字母的异位词(固定长度的滑动窗口)leetcode567:字符串的排列与该题思路相同
    • leetcode930:和相同的二元子数组
    • leetcode5739:最高频元素的频数
    • leetcode239:滑动窗口最大值
    • 剑指Offer59—II:队列的最大值(单调双向队列)
    • leetcode5740:所有元音按顺序排布的最长子字符串
    • leetcode904:水果成篮
    • leetcode252:会议室
    • leetcode253:会议室II
    • 典型差分:上下车、上下飞机、上下台阶
    • leetcode379:区间加法
    • leetcode1109:航班预定统计(就是区间加法)
    • leetcode1094:拼车
  • 【模拟行为】
    • leetcode54:螺旋矩阵
    • leetcode59:螺旋矩阵II
  • 排序
  • 排序算法专题
    • 1.冒泡排序
    • 2.选择排序
    • 3.插入排序
    • 4.希尔排序
    • 5. 归并排序【重要】
      • 归排序背诵模板
    • 6.快速排序【重要】
      • 快排背诵模板
    • 7.堆排序
      • 为什么使用优先队列?
      • **堆的细节优化**
      • 堆排序模板
      • 索引堆
    • 8.计数排序
    • 9.桶排序
    • 10.基数排序
  • leetcode75:颜色分类
  • leetcode88:合并两个有序数组

第一章:数组

【二分查找法】

leetcode704:二分查找

给定一个 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]之间。

【二分查找法的模板套路】

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size();
        while(right >= left)
        {
            int mid = left + (right - left) / 2;
            if(nums[mid] == target)
                return mid;
            if(nums[mid] > target)
            {
                right = mid - 1;
            }
            else
            {
                left = mid + 1;
            }
        }
        return -1;
    }
};

剑指Offer53_II

【题目】

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

 

示例 1:

输入: [0,1,3]
输出: 2
示例 2:

输入: [0,1,2,3,4,5,6,7,9]
输出: 8
 

限制:

1 <= 数组长度 <= 10000


//时间复杂度O(n)
//空间复杂度O(1)
class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int left = 0, right = nums.size()-1;
        int mid = 0;
        while(right >= left)
        {
            mid = left + (right - left) / 2;
            if(nums[mid] == mid)
            {
                left = mid + 1;
            }
            else
            {
                right = mid - 1;
            }
        }
        return left;
    }
};

时间复杂度:O(n)
空间复杂度:O(1)
class Solution {
public:
    int missingNumber(vector<int>& nums) {
        int len = nums.size();
        for(int i = 0; i < len; i++)
        {
            if(nums[i] != i)
            {
                return i;
            }
        }
        return len;
    }
};

leetcode1011:在D天内送达包裹的能力

补充:a/b默认的是向下取整
但是如果是向上取整的话需要的是[a+b-1/b]代表的是a/b向上取整,也可以ciel
算法中能不用浮点数就不要用浮点数

传送带上的包裹必须在 D 天内从一个港口运送到另一个港口。

传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量的顺序往传送带上装载包裹。我们装载的重量不会超过船的最大运载重量。

返回能在 D 天内将传送带上的所有包裹送达的船的最低运载能力

示例 1:

输入:weights = [1,2,3,4,5,6,7,8,9,10], D = 5
输出:15
解释:
船舶最低载重 15 就能够在 5 天内送达所有包裹,如下所示:
第 1 天:1, 2, 3, 4, 5
第 2 天:6, 7
第 3 天:8
第 4 天:9
第 5 天:10

请注意,货物必须按照给定的顺序装运,因此使用载重能力为 14 的船舶并将包装分成 (2, 3, 4, 5), (1, 6, 7), (8), (9), (10) 是不允许的。
示例 2:

输入:weights = [3,2,2,4,1,4], D = 3
输出:6
解释:
船舶最低载重 6 就能够在 3 天内送达所有包裹,如下所示:
第 1 天:3, 2
第 2 天:2, 4
第 3 天:1, 4
示例 3:

输入:weights = [1,2,3,1,1], D = 4
输出:3
解释:
第 1 天:1
第 2 天:2
第 3 天:3
第 4 天:1, 1

提示:

1 <= D <= weights.length <= 50000
1 <= weights[i] <= 500

[分析]

为什么能考虑用二分查找,是因为当mid值(负载最小能力)大于负载时

class Solution {
    bool check(vector<int> weights, int D, int mid)
    {
         //need为需要运送的天数
         //cur为当前这一天已经运送的包裹重量之和
        int need = 1, cur = 0;//共需要多少天,肯定时大于等于1的整数;cur当前船载重
        for(int weight : weights)
        {
            if(cur + weight > mid)
            {
                need++;
                //因为天数增加了,到了明天肯定船的载重重置了
                cur = 0;
            }
            cur += weight;
        }
        return need <= D;
    }
public:
    int shipWithinDays(vector<int>& weights, int D) {
        //确定二分查找的边界
        int left = *max_element(weights.begin(), weights.end()),right = accumulate(weights.begin(), weights.end(), 0);
        while(right >= left)
        {
            // 求得中点
            int mid = left + (right - left) / 2;
            //如果需求的天数小于要求的天数说明mid值选大了,或者说最低运载能力选大了
            // 可以在D天内运送完毕,则mid是一个合法的答案
            // 记录下当前答案,并套用规律2,不再考虑比它大的数字
            // 移动 right 右边界
            //因为是cur + weight > mid 所以mid值一定是复合要求的所以记录。right就没有必要等于mid,这样也会引起死循环
            if(check(weights, D, mid))
            {
                right = mid - 1;
            }
            //如果需求的天数大于D天,说明mid值选小了,需要扩大最低运载能力
            // 不可以在D天内运送完毕
            // 此时套用规律2,不再考虑比它小的数字
            // 移动 left 左边界
            else
            {
                left = mid + 1;
            }
        }
        return left;//因为运行到这里肯定left是满足条件的最小值;left与right错位差1
    }
};

leetcode1482:制作m束花所需的最少天数(与leetcode1011:解题思路相同)

【题目】

给你一个整数数组 bloomDay,以及两个整数 m 和 k 。

现需要制作 m 束花。制作花束时,需要使用花园中 相邻的 k 朵花 。

花园中有 n 朵花,第 i 朵花会在 bloomDay[i] 时盛开,恰好 可以用于 一束 花中。

请你返回从花园中摘 m 束花需要等待的最少的天数。如果不能摘到 m 束花则返回 -1 。

示例 1:

输入:bloomDay = [1,10,3,10,2], m = 3, k = 1
输出:3
解释:让我们一起观察这三天的花开过程,x 表示花开,而 _ 表示花还未开。
现在需要制作 3 束花,每束只需要 1 朵。
1 天后:[x, _, _, _, _] // 只能制作 1 束花
2 天后:[x, _, _, _, x] // 只能制作 2 束花
3 天后:[x, _, x, _, x] // 可以制作 3 束花,答案为 3
示例 2:

输入:bloomDay = [1,10,3,10,2], m = 3, k = 2
输出:-1
解释:要制作 3 束花,每束需要 2 朵花,也就是一共需要 6 朵花。而花园中只有 5 朵花,无法满足制作要求,返回 -1 。
示例 3:

输入:bloomDay = [7,7,7,7,12,7,7], m = 2, k = 3
输出:12
解释:要制作 2 束花,每束需要 3 朵。
花园在 7 天后和 12 天后的情况如下:
7 天后:[x, x, x, x, _, x, x]
可以用前 3 朵盛开的花制作第一束花。但不能使用后 3 朵盛开的花,因为它们不相邻。
12 天后:[x, x, x, x, x, x, x]
显然,我们可以用不同的方式制作两束花。
示例 4:

输入:bloomDay = [1000000000,1000000000], m = 1, k = 1
输出:1000000000
解释:需要等 1000000000 天才能采到花来制作花束
示例 5:

输入:bloomDay = [1,10,2,9,3,8,4,7,5,6], m = 4, k = 2
输出:9

提示:

bloomDay.length == n
1 <= n <= 10^5
1 <= bloomDay[i] <= 10^9
1 <= m <= 10^6
1 <= k <= n

class Solution {
    bool check(vector<int> bloomDay, int m, int k, int day)
    {
        int flower = 0;
        int bouquet = 0;
        for(int b : bloomDay)
        {
            if(b <= day)
            {
                flower++;
                if(flower == k)
                {
                    bouquet++;
                    flower = 0;
                }
            }
            else
            {
                flower = 0;
            }
        }
        return bouquet >= m;
    }
public:
    int minDays(vector<int>& bloomDay, int m, int k) {
        int len = bloomDay.size();
        if(m * k > len)
        {
            return -1;
        }

        int left = INT_MAX, right = INT_MIN;
        for(int i = 0; i < len; i++)
        {
            left = min(bloomDay[i], left);
            right = max(bloomDay[i], right);
        }
        
        while(left <= right)
        {
            int mid = left + (right - left) / 2;
            if(check(bloomDay, m, k, mid) == true)
            {
                right = mid - 1;
            }
            else
            {
                left = mid + 1;
            }
        }
        return left;
    }
};

leetcode1733:完成所有工作的最短时间

本题与leetcode1011:很相似,有一点不同的就是在1011题中,是顺序使用数组中的数字,而本题需要使用满足条件的数字,这个不同就导致了本题使用了回溯+剪枝,而上一题就没有使用回溯

给你一个整数数组 jobs ,其中 jobs[i] 是完成第 i 项工作要花费的时间。

请你将这些工作分配给 k 位工人。所有工作都应该分配给工人,且每项工作只能分配给一位工人。工人的 工作时间 是完成分配给他们的所有工作花费时间的总和。请你设计一套最佳的工作分配方案,使工人的 最大工作时间 得以 最小化 。

返回分配方案中尽可能 最小 的 最大工作时间 。

示例 1:

输入:jobs = [3,2,3], k = 3
输出:3
解释:给每位工人分配一项工作,最大工作时间是 3 。
示例 2:

输入:jobs = [1,2,4,7,8], k = 2
输出:11
解释:按下述方式分配工作:
1 号工人:1、2、8(工作时间 = 1 + 2 + 8 = 11)
2 号工人:4、7(工作时间 = 4 + 7 = 11)
最大工作时间是 11 。

提示:

1 <= k <= jobs.length <= 12
1 <= jobs[i] <= 10^7

//二分查找+回溯+剪枝
class Solution {
    //jobs:工作量,workloads:记录每人工作时间;index:jobs下标;limit:最大工作时间
    bool backtrack(vector<int>& jobs, vector<int>& workloads, int index, int limit)
    {
        if(index >= jobs.size())
        {
            return true;
        }
        int cur = jobs[index];//当前的工作强度
        for(int& workload : workloads)
        {
           if(workload + cur <= limit)
           {
                workload += cur;
            if(backtrack(jobs, workloads, index+1, limit))//与leetcode1011的区别就是,此题并不是顺序使用数字,而是只要满足条件就使用。
            {
                return true;
            }
            workload -= cur;
            // 如果当前工人未被分配工作,那么下一个工人也必然未被分配工作
            // 或者当前工作恰能使该工人的工作量达到了上限
            // 这两种情况下我们无需尝试继续分配工作
           }
           //如果下面剪枝代码不写会超时
            if(workload == 0 || workload + cur == limit)
                break;
        }
        return false;
    }
    //jobs工作量,k:工人数,limit:最大工作时间
    bool check(vector<int>& jobs, int k, int limit)
    {
        vector<int> workloads(k, 0);
        return backtrack(jobs, workloads, 0, limit);
    }
public:
    int minimumTimeRequired(vector<int>& jobs, int k) {
        sort(jobs.begin(), jobs.end(), greater<int>());//将jobs从大到小排序;如果不写greater默认是从小到大
        int l = jobs[0], r = accumulate(jobs.begin(), jobs.end(), 0);
        while(l <= r)
        {
            int mid = l + (r - l) / 2;
            if(check(jobs, k, mid))
            {
                r = mid - 1;
            }
            else
            {
                l = mid + 1;
            }
        }
        return l;
    }
};

leecode69:x的平方根

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:

输入: 4
输出: 2
示例 2:

输入: 8
输出: 2
说明: 8 的平方根是 2.82842…,
由于返回类型是整数,小数部分将被舍去。

class Solution {
    //证明:当x大于1时,总有x/2 * x/2 >= x, x是整数
public:
    int mySqrt(int x) {
        //res 属于[0, x/2]使用二分查找找到这个值
        assert(x >= 0);
        //特殊处理的数据
        if(x == 1)
            return 1;
        int left = 0, right = x/2;
        while(right >= left)
        {
            long long mid = left + (right - left) / 2;
            if(mid * mid == x)//凡是没有规定数据量的,并且有两个数的加法或者乘法的一定判断是否越界
                return mid;
            if(mid * mid > x)//凡是没有规定数据量的,并且有两个数的加法或者乘法的一定判断是否越界
            {
                right = mid - 1;
            }
            //mid*mid可能等于的情况在下面
            else//凡是没有规定数据量的,并且有两个数的加法或者乘法的一定判断是否越界
            {
                left = mid + 1;
            }
        }
        //运行到这里肯定时left与right相互错开,都没有找到mid*mid=x,所以结果肯定时right或者是left
        return right;
    }
};

leetcode29:两数相除

给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。

返回被除数 dividend 除以除数 divisor 得到的商。

整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2

 

示例 1:

输入: dividend = 10, divisor = 3
输出: 3
解释: 10/3 = truncate(3.33333..) = truncate(3) = 3
示例 2:

输入: dividend = 7, divisor = -3
输出: -2
解释: 7/-3 = truncate(-2.33333..) = -2
 

提示:

被除数和除数均为 32 位有符号整数。
除数不为 0。
假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−2^31,  2^31 − 1]。本题中,如果除法结果溢出,则返回 2^31 − 1。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/divide-two-integers
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

【思路】

由于题目限制不可以使用乘法与除法

所以乘法是用位运算实现,然后利用对于x除以y,结果x/y必然落入在[0, x]范围内,对这个区间进行二分:

class Solution {
    //实现乘法操作,使用倍乘,位运算实现乘法操作
    long mul(long a, long k)
    {
        long ans = 0;
        while(k > 0)
        {
            //判断奇数
            if(k & 1)
                ans += a;
            k >>= 1;
            a += a;
        }
        return ans;
    }
public:
    int divide(int dividend, int divisor) {
        //因为可能会越界所以用long
        long x = dividend, y = divisor;
        //isNeg表示的是两数相除结果的符号,默认为正号
        bool isNeg =  false;
        if(x < 0 && y > 0 || x > 0 && y < 0)
        {
            //符号变为符号
            isNeg = true;
        }
        //将x于y均变成正数
        if(x < 0) x = -x;
        if(y < 0) y = -y;
        long left = 0, right = x;
        int result = 0;
        //两数相除的结果肯定在0到x之间,所以使用二分法查找这个值
        while(left <= right)
        {
            long mid = left + (right - left) / 2;
            if(mul(mid, y) <= x)
            {
     	//注意:对于近似的二分搜索,要用result取接住临时mid值,否则可能会出现忽略某个值而无解的情况。
                result = mid;
                left = mid + 1;
            }
            else
            {
                right = mid - 1;
            }
        }
        //值得注意的是int_min可以包容int_max,而int_max是不能包容int_min的
        if(isNeg && result <= INT_MIN )
           return INT_MIN;
        if(!isNeg && result <= INT_MIN )
           return INT_MAX;
        
        if(isNeg)
            result = -result;
        return (int)result;
    }
};

typedef long long LL;
class Solution {
    LL mul(LL a, LL b)
    {
        LL res = 0;
        while(a > 0)
        {
            if(a & 1)
                res += b;
            a >>= 1;
            b += b;
        }
        return res;
    }
public:
    int divide(int dividend, int divisor) {
        LL x = dividend, y = divisor;
        bool isNeg= false;
        if(x < 0 && y > 0 || x > 0 && y < 0)
            isNeg = true;
        if(x < 0) x = -x;
        if(y < 0) y = -y;
        LL l = 0, r = x;
        while(l <= r)
        {
            LL mid = (l + r) / 2;
            if(mul(y, mid) == x)
            {
                r = mid;
                break;
            }
            else if(mul(y, mid) < x)
                l = mid + 1;
            else 
                r = mid - 1;
        }
        //因为是int所以容易爆int型
        if(isNeg && r <= INT_MIN)
            return INT_MIN;
        if(!isNeg && r >= INT_MAX)
            return INT_MAX;
        if(isNeg)
            return -r;
        return r;
    }
};

leetcode33:搜索旋转排序数组

【题目】

整数数组 nums 按升序排列,数组中的值 互不相同 。(有序数组,互不相同)

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组**【输入】nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回【输出】它的下标**,否则返回 -1 。

示例 1:

输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:

输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:

输入:nums = [1], target = 0
输出:-1

提示:

1 <= nums.length <= 5000
-10^4 <= nums[i] <= 10^4
nums 中的每个值都 独一无二
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-10^4 <= target <= 10^4

进阶:你可以设计一个时间复杂度为 O(log n) 的解决方案吗?----》二分查找法

【思路】

就是找到顺序排序的区间,在该区间内用二分查找法

【算法思路】

将数组从中间分开成左右两个部分的时候,一定有一部分的数组是有序的。拿示例来看,我们从6这个位置分开以后数据变成了[4,5,6]和[7,0,1,2]两个部分,其中左边[4,5,6]这个部分的数组是有序的,其他也是如此。

这启示我们可以在常规二分法的时候查看当前mid为分割位置分割出来的两个部分[1,mid]和[mid+1, r]那部分是有序的,并根据有序

class Solution {
public:
    int search(vector<int>& nums, int target) {

        //查找下标用二分查找法:
        int left = 0; 
        int right = nums.size() - 1;

        while(right >= left){//边界条件,一定是等于这样,[left, right]
            int mid = left + (right - left ) / 2;
            if(nums[mid] == target)//[mid, mid]已经,如果找到目标
                return mid;

            //需求里说明了:没有重复数字
            //[left mid]是顺序排列的
            if(nums[left] <= nums[mid])//边界条件有等于号-》left~mid是递增的用二分查找
            {
                if(nums[left] <= target && target < nums[mid])
                {//target 属于 [left, mid)
                    right = mid - 1;//因为target < nums[mid]所以才大胆的right = mid - 1而不是right = mid
                }
                else
                { 
                    left = mid + 1;
                }
            }
            else //nums[left] > nums[mid]-->mid~right是递增的,用二分查找
            {
                if(nums[mid] < target && target >= nums[right])
                {//target 属于 (mid, right]
                    left = mid + 1;
                }
                else
                {
                    right = mid - 1;
                }
            }
        }
        return -1;
    }
};

leetcode81:搜索旋转排序数组II

【题目】

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。

给你 **(输入)旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则(输出)**返回 true ,否则返回 false

示例 1:

输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
示例 2:

输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false

提示:

1 <= nums.length <= 5000
-10^4 <= nums[i] <= 10^4
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-10^4 <= target <= 10^4

进阶:

这是 搜索旋转排序数组 的延伸题目,本题中的 nums 可能包含重复元素
这会影响到程序的时间复杂度吗?会有怎样的影响,为什么?

【解析】

class Solution {
public:
    bool search(vector<int>& nums, int target) {
        //二分查找法精髓
        int left = 0;
        int right = nums.size() - 1;

        while(left <= right)
        {
            //中间值
            int mid = left + ( right - left) / 2;
            //如果相等就返回,二分查找法的套路
            if(target == nums[mid]){
                return true;
            }
            //因为需要上说可能包含重复元素所以相比较常规的二分查找法多了判断相等时的条件
            if(nums[left] == nums[mid] && nums[mid] == nums[right])//重复数字
            {
                left++;
                right--;
            }
            else if(nums[left] <= nums[mid])//->left-mid是有序的
            {
                //二分查找法
                if(nums[left] <= target && target < nums[mid] )//左闭右开
                {
                    right = mid - 1;
                }
                else 
                {
                    left = mid + 1;
                }
            }
            else//--》mid-right是有序的
            {
                //二分查找法
                if(nums[mid] <= target && target <= nums[right])//左闭右闭,否则会把右边界给忽略掉
                {               
                    left = mid + 1;
                }
                else 
                {
                    right = mid - 1;
                }
            }
        }
        return false;
    }
};

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

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

进阶:

你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:

输入:nums = [], target = 0
输出:[-1,-1]

提示:

0 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
nums 是一个非递减数组
-10^9 <= target <= 10^9

**难点一:**考虑当len==0时,程序该怎么处理

难点二:所以使用数组的时候都要考虑是否数组越界,例如right >=0 && nums[right] == targetright >= 1 && nums[right] == nums[right - 1]

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int len = nums.size();
        if(len == 0)
            return {-1, -1};
        int left = 0, right = len - 1;
        int begin = -1 , end = -1;
        //找到nums中等于target最右侧的值的下标
        while(right >= left)
        {
            int mid = left + (right - left) / 2;
            if(nums[mid] > target)
                right = mid - 1;
            else
            {
                left = mid + 1;
            }
        }
        //如果数组用上了一定要判断是否大于0,因为数组下标不可能小于零
        if(right >=0 && nums[right] ==  target)//如果数组用到了某个值一定要判断数组是否越界
        {
            end = right;
            while(right >= 1 && nums[right] == nums[right - 1])//如果数组用到了某个值一定要判断数组是否越界
                right--;
            begin = right;
        }
        return {begin, end};
        
        /*
        end = right;
        //right 就是最后一个位置
        while(right >= 0 && nums[right] == target)
        {
            right--;
        }
        begin = right + 1;
        if(end >= begin)
            return {begin, end};
        return {-1, -1};
        */
    }
};

leetcode:35:搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

示例 1:

输入: [1,3,5,6], 5
输出: 2
示例 2:

输入: [1,3,5,6], 2
输出: 1
示例 3:

输入: [1,3,5,6], 7
输出: 4
示例 4:

输入: [1,3,5,6], 0
输出: 0

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        while(right >= left)
        {
            int mid = left + ( right - left ) / 2;
            if(nums[mid] > target)
            {
                right = mid - 1;
            }
            else if(nums[mid] < target)
            {
                left = mid + 1;
            }
            else
            {
                return mid;
            }
        }
        //犯错就是这里并不是left-1!!!!
        return left;//如果运行到这里肯定是left > right这种错位大小。
    }
};

leetcode153:寻找旋转排序数组中的最小值

【题目】

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 4 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

示例 1:

输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:

输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:

输入:nums = [11,13,15,17]
输出:11
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。

提示:

n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 中的所有整数 互不相同
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转

【思路】

将数据转成折线图:

数据结构与算法(一)_第1张图片

我们考虑数组中的最后一个元素 x:在最小值右侧的元素(不包括最后一个元素本身),它们的值一定都严格小于 x;而在最小值左侧的元素,它们的值一定都严格大于 x。因此,我们可以根据这一条性质,通过二分查找的方法找出最小值。

第一种情况

数据结构与算法(一)_第2张图片

第二种情况

数据结构与算法(一)_第3张图片

class Solution {
public:
    int findMin(vector<int>& nums) {
        //暴力法
        //时间复杂度:O(n)
        //空间复杂度:O(1)
        /*
        int min = INT_MAX;
        for(int i = 0; i < nums.size(); i++){
            if(nums[i] < min)
            {
                min = nums[i];
            }
        }
        return min;*/
        //因为在某一区间内满足有序条件,所以自然而然想到二分查找法
        //如果输入为空-》怎么处理
         int left = 0, right = nums.size() - 1;
        int res = INT_MAX;//存储最小值
        while(right >= left)
        {
            int mid = left + (right - left) / 2;
            //如果nums[mid] >= nums[left]则在left到mid区间内一定是递增的,所以nums[left]一定是最小值,后面的right一定等于mid不能是mid-1,因为mid可能就是最小值,如果等于mid-1,对于mid最小值的情况就会忽略从而报错
            if(nums[mid] >= nums[left] )
            {
                //验证res是否是最小值,因为nums[left]相比较nums[mid]就是最小值
                if(res > nums[left])
                    res = nums[left];
                left = mid + 1;
            }
            else
            {
                right = mid;
            }
        }
        return res;
    }
};

class Solution {
public:
    int findMin(vector<int>& nums) {
        int l = 0, r = nums.size() - 1;
        int res = INT_MAX;
        while(r > l)
        {
            int mid = (l + r) / 2;
            if(nums[r] < nums[mid])
            {
                
                l = mid + 1;
            }
            else 
                r = mid;
        }
        return nums[l];
    }
};

leetcode154 寻找旋转排序数组中的最小值II

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

示例 1:

输入:nums = [1,3,5]
输出:1
示例 2:

输入:nums = [2,2,2,0,1]
输出:0

提示:

n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转

进阶:

这道题是 寻找旋转排序数组中的最小值 的延伸题目。
允许重复会影响算法的时间复杂度吗?会如何影响,为什么?

【思路】

相比上一题来看,只是增加条件数组中允许重复数字的存在。相比较153题只需要考虑当**nums[mid] == nums[right]**的情况时怎么处理

class Solution {
public:
    int findMin(vector<int>& nums) {
       /*
        int min_result = INT_MAX;
        for(int i = 0; i < nums.size(); i++){
            if(nums[i] < min_result)
                min_result = nums[i]; 
        }
        return min_result;
       */

        //因为旋转之前是有序数组,所以可以考虑一下二分查找法
        int left = 0, right = nums.size() - 1;
		int res = INT_MAX;
        //因为用到了nums[right]所以一定要确保right > left
        //[10,2,10,10,10]这种类型的数据不通过采用的下面的方式解决
        while(left < right && nums[0] == nums[right]) 
            right--;
		while (right >= left)
		{
			int mid = left + (right - left) / 2;
			if (nums[mid] >= nums[left])
			{
				if (res > nums[left])
					res = nums[left];
				left = mid + 1;
			}
			else 
			{
				right = mid;//如果right = mid - 1的话,就会把mid是最小值的情况给忽略
			}
		}
		return res;
    }
};

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0, right = nums.size() - 1;
        while(left < right)
        {
            int mid = (left + right) / 2;
            if(nums[mid] > nums[right])
                left = mid + 1;
            else if(nums[mid] < nums[right])
                right = mid;
            else    
                right --;
        }
        return nums[left];
    }
};

【双指针】

【本质】双指针(快慢指针法):通过一个快指针和满指针在一个for循环下完成两个for循环的工作。

双指针算法的模板

for(int i = 0, j = 某个值; i < n; i++)
{
	while(j < i && check(i, j)) j++;
	//每道题的逻辑
}

可以将O(n*n)的算法简化为O(n)

leetcode283:移动零

给定一个**【输入】数组 nums**,编写一个函数**【目的】将所有 0 移动到数组的末尾**,同时保持非零元素的相对顺序。

示例:

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:

必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。

【思考】

因为限制,采用双指针的方法,如果出现在原数组上进行操作,不能创建额外空间,百分之八十双指针

【代码】

//leetcode283:移动零
class Solution2 {
public:
	//时间复杂度:0(n)
	//空间复杂度:O(n)
	void moveZeroes(vector<int>& nums) {
		vector<int> nonZeroElements;
		for (int i = 0; i < nums.size(); i++) {
            //只要nums[i]不为0就像vector中插入元素
			if (nums[i]) {
				nonZeroElements.push_back(nums[i]);
			}
		}
        
		for (int i = 0; i < nonZeroElements.size(); i++) {
			nums[i] = nonZeroElements[i];
		}
		for (int i = nonZeroElements.size(); i < nums.size(); i++) {
			nums[i] == 0;
		}
	}
};
class Solution3 {//双指针
public:
	//时间复杂度:0(n)
	//空间复杂度:O(1)
	void moveZeroes(vector<int>& nums) {
        for(int right = 0, left = 0; right < nums.size(); right++)
        {
            //不管做指针是否指向0,只管right指针对应的值,
            //思考:有没有可能nums[right]越界,所以定义的循环变量是right
            if(nums[right] != 0)
            {
                swap(nums[left], nums[right]);
                left++;
            }
        }
	}
};

//练习题:26;27;80;
void main_283() {
	vector<int> nums = { 1,0,3,12,0 };
	Solution2().moveZeroes(nums);
	system("pause");
	return;
}

leetcode26:删除排序数组中的重复项

给定一个排序数组(很关键),有的时候就是双指针与排序数组相结合,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

示例 1:

给定数组 nums = [1,1,2], 

函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。 

你不需要考虑数组中超出新长度后面的元素。
示例 2:

给定 nums = [0,0,1,1,1,2,2,3,3,4],

函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。

你不需要考虑数组中超出新长度后面的元素。
//创建额外数组来解决问题
class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int left = 0, right = 0;
        //异常处理
        if(nums.size() == 0)return 0;
        //特殊情况处理
        vector<int> result;
        result.push_back(nums[right]);
        while(right < nums.size()){
            //寻找不重复的值,对于数组一定注意检查是否越界,right>0;
            if(right > 0 && nums[right] != nums[right - 1]){
                result.push_back(nums[right]);
            }
            right++;
        }
        nums = result;
        return result.size();
    }
};
//双指针+有序数组
class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int left, right;
        if(nums.size() == 0)
            return 0;
        for(left = 0, right = 0; right < nums.size(); right++)
        {
            if(nums[left] != nums[right])
            {
                left ++;
                swap(nums[left], nums[right]);
            }
        }
        return left + 1;
    }
};
void main() {
	vector<int>nums = { 0,0,1,1,1,2,2,3,3,4 };
	Solution().removeDuplicates(nums);
	return;
}

leetcode27:移除元素

给你一个数组 nums一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

【思路】使用双指针中的快慢指针

//创建额外数组
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        vector<int> result;
        for(int i = 0; i < nums.size(); i++){
            if(nums[i] != val){
                result.push_back(nums[i]);
            }
        }
        nums = result;
        return nums.size();
    }
};

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int left = 0, right = 0;
        for(; right < nums.size(); right++)
        {
        /*
            if(nums[left] != val) 
                left++;
            else if(nums[right] != val )
            {
                swap(nums[left], nums[right]);
                left++;
            }   
            优化*/
            if(nums[right] != val )
            {
                swap(nums[left], nums[right]);
                left++;
            }   
        }
        return left;
    }
};

void main_27() {
	vector<int>nums = { 2,3,2,3 };
	Solution_27().removeElement(nums, 3);
	return;
}

leetcode80:删除排序数组中重复项II(做错很多次)

【题目】

给定一个增序排列数组 nums ,你需要在 原地 删除重复出现的元素,使得每个元素最多出现两次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]

解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3 。你不需要考虑数组中超出新长度后面的元素。
提示:

0 <= nums.length <= 3 * 10^4
-10^4 <= nums[i] <= 10^4
nums 按递增顺序排列

【思路分析】

首先,用变量maxRepeat=2来表示每个元素最多出现两次。定义指针slow,其初始值为maxRepeat-1=1。

数据结构与算法(一)_第4张图片

规定区间[0,slow]内的元素为最多出现两次的元素。

数据结构与算法(一)_第5张图片

定义变量fast=maxRepeat=2,指向当前考察的元素。

数据结构与算法(一)_第6张图片

当前考察的元素nums[fast]与nums[slow-maxRepeat-1]相等,即变量fast所指向的元素1在区间[0,slow]中已出现两次。

数据结构与算法(一)_第7张图片

因此继续考察下一个元素,fast向右移动一位。此时nums[fast] != nums[slow - maxRepeat + 1],即指针fast指向的元素2不等于指针slow - maxRepeat + 1所指向的元素1,这时快指针需要停下来等待。

数据结构与算法(一)_第8张图片

区间[0,slow]内的元素为最多出现两次的元素,因此需要将慢指针slow向右移动一位来扩大区间。

接着将快指针fast指向的元素值赋予慢指针slow所指向的位置。

对于数组中剩余未考察的元素2,3重复上述步骤即可使得每个元素最多出现两次。

计算出错的原因是这个是赋值并不是交换。

赋值:nums[left] = nums[right]

交换:swap(nums[left], nums[right])

时间复杂度O(n)
空间复杂度O(1)
class Solution {
public:
	int removeDuplicates(vector<int>& nums) {
		//初始化
		if (nums.size() <= 2)return nums.size();
		int left= 1; int maxRepeat = 2;
		//过程
		for (int right = 0; right< nums.size(); right++) {
            //如果nums[right ] == nums[left- max + 1]我们就将fast指针移动,目的是将重复的元素留在中间
            //nums[right] != nums[left- max + 1]做赋值操作
			if (right >= 2 && nums[right] != nums[left- maxRepeat + 1]) {
                left++;
				nums[left] = nums[right];
			}
		}
        //一定要+1因为数组的下标是从0开始的
		return left + 1;
	}
};

void main() {
	vector<int>nums = { 1,1,1,2,2,3 };
	int result = Solution().removeDuplicates(nums);
	system("pause");
	return;
}

leetcode844:比较含退格的字符串

给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。

注意:如果对空文本输入退格字符,文本继续为空。

示例 1:

输入:S = “ab#c”, T = “ad#c”
输出:true
解释:S 和 T 都会变成 “ac”。
示例 2:

输入:S = “ab##”, T = “c#d#”
输出:true
解释:S 和 T 都会变成 “”。
示例 3:

输入:S = “a##c”, T = “#a#c”
输出:true
解释:S 和 T 都会变成 “c”。
示例 4:

输入:S = “a#c”, T = “b”
输出:false
解释:S 会变成 “c”,但 T 仍然是 “b”。

提示:

1 <= S.length <= 200
1 <= T.length <= 200
S 和 T 只含有小写字母以及字符 ‘#’。

【思路】

我们遍历过程,每次遍历一个字符:

  • 如果它是退格符,那么我们就将栈顶弹出
  • 如果它是普通字符,那么我们就将其压入栈中

**注意:**字符串也是一种特殊数组,数组的一些操作均可用在字符串上

//时间复杂度:O(M+N),M与N分别位字符串S与T的长度。我们需要遍历两个字符串各一次
//空间复杂度:O(M+N),M与N分别位字符串S与T的长度。主要为还原出字符串的开销
class Solution {
    string build(string str)
    {
        string ret;
        for(char ch : str)
        {
            if(ch != '#')
            {
                ret.push_back(ch);
            }
            else if(!ret.empty())
            {
                ret.pop_back();
            }
        }
        return ret;
    }
public:
    bool backspaceCompare(string s, string t) {
        return build(s) == build(t);
    }
};

leetcode633:平方数之和

给定一个**非负整数 c** ,你要判断**是否存在两个整数 a 和 b,使得 a^2 + b^2 = c** 。

示例 1:
输入:c = 5
输出:true
解释:1 * 1 + 2 * 2 = 5
示例 2:
输入:c = 3
输出:false
示例 3:
输入:c = 4
输出:true
示例 4:
输入:c = 2
输出:true
示例 5:
输入:c = 1
输出:true
提示:
0 <= c <= 2^31 - 1

【思路】

从数据规模来分析,可能会出现大数据的情况,所以需要考虑在内

//二分查找法
class Solution 
    bool search(int x, int target)
    {
        int left = 0;
        int right = x;
        while(right >= left)
        {
            long long mid = left + (right - left) / 2;
            if(mid * mid == target)
                return true;
            if(mid * mid > target)
            {
                right = mid - 1;
            }
            else
            {
                left = mid + 1;
            }
        }
        return false;
    }
public:
    bool judgeSquareSum(int c) {
        cout<<INT_MAX<<endl;
        long long temp = 0;
        if(c == 0 || c == 1)
            return true;
        for(long long i = 0; i <= c/2; i++ )
        {
            temp = i * i;
            if((i * i < INT_MAX) && search(c , c - temp))
                return true;
        }
        return false;
    }
    //库函数法
    public:
    bool judgeSquareSum(int c) {
        //下面两种方法的时间复杂度为根号c
        //cout<
        //枚举法:不要吝啬使用c++自身带的库函数
        //因为a要小于等于根号下c,b也要小于等于根号下c
        int a_Max = sqrt(c);
        for(int i = 0; i <= a_Max; i++)
        {
            int temp_b_b = sqrt(c - i * i);
            if(temp_b_b * temp_b_b + i * i == c)
                return true;
        }
        return false;
        
        //双指针法
        long long a = 0, b = (int)sqrt(c);//必须将sqrt(c)强制转换成int型,否则b会是一个小数,与题目b是整数不符
        cout<<INT_MAX<<endl;
        long long cur = -1;
        while(a <= b)
        {
            cur = a * a + b * b;
            if(cur == c)
            {
                return true;
            }
            else if(cur > c)
                b--;
            else if(cur < c)
                a++;
        }
        return false;
    }
};

面试题17.21:直方图水量(接雨水)

该题两个下标都在变化,是模板的延申
【题目】

给定一个直方图(也称柱状图),假设有人从上面源源不断地倒水,最后直方图能存多少水量?直方图的宽度为 1。

数据结构与算法(一)_第9张图片

上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的直方图,在这种情况下,可以接 6 个单位的水(蓝色部分表示水)。

示例:

输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
class Solution {
public:
    int trap(vector<int>& height) {
        int left = 0; 
        int right = height.size() - 1;
        int High = 1;
        int Sum = 0;
        //用于求黑色圆柱体的体积
        for(int i = 0; i <= right ; i++)
        {
            Sum += height[i];
        }
        //用于求最大的容积
        int volume = 0;
         //这个等号是为了计算只有一个柱子的面积,如果不加等号,将会少计算柱子的体积
        while(left <= right)
        {
            //其余的不管我只计算大的容量
            //如果输入为[0,3,0,2,0,3]同样就不计算2了直接计算3到3的体积
             //这两个放等号就很灵性,这样后面当right < left时的right-left+1就会变成0了
            while(left <= right && height[left] < High)
            {
                left++;
            }
            while(left <= right && height[right] < High)
            {
                right--;
            }
            //第一次循环计算的是高度为1左右的容量
            volume += right - left + 1;
            High++;//第一次High++后High=2就是计算高度为2左右的容量
        }
        return volume - Sum;
    }
};

leetcode167:两数之和II(与平方数之和解法相同就是不一样的描述)

【题目】

给定一个已按照 升序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target

函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length 。

你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。该句话不说就要询问

示例 1:

输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
示例 2:

输入:numbers = [2,3,4], target = 6
输出:[1,3]
示例 3:

输入:numbers = [-1,0], target = -1
输出:[1,2]

提示:

2 <= numbers.length <= 3 * 10^4
-1000 <= numbers[i] <= 1000
numbers 按 递增顺序 排列
-1000 <= target <= 1000
仅存在一个有效答案

思考:

  • 如果没有解怎样处理
  • 如果由多个解怎样返回值

【方法一:暴力解】双层遍历O(n^2)

超时没有充分利用原数组的性质,有序性

二分查找法需要注意的两点:

  1. 去除重复取值的情况
  2. 得到的数组要进行从小到达的排序,这里需求没有说
//正解
class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        //初始化变量
        int len = numbers.size();
        int begin_index = 0; 
        int end_index = len-1;

        //循环遍历
        while(begin_index < end_index){
            //因为是有序数组所以分为三种情况
            //情况一:之和大于目标值-》解决是将end_index--
            if(numbers[begin_index] + numbers[end_index] > target)
                end_index--;
            //情况二:之和小于目标值-》解决是将begin_index++;
            else if(numbers[begin_index] + numbers[end_index] < target)
                begin_index++;
            //情况三:之和等于目标值满足题目输出-》直接return
            else{
            //特殊情况因为题目要求说数组下标从1开始算,所以下面的指针都+1
                return {begin_index + 1, end_index + 1};
            }
        }
        //如果没有找到之和等于目标值的时候直接返回空数组
        return {};
    }
};

};
void main() {
	vector<int> nums = { 2,7,11,15 };
	Solution().twoSum(nums, 9);
	nums =  Solution().twoSum_II(nums, 9);
	return;
}
class Solution {
	//二分查找法->一定是一个有序的数组
    int binarySearch(vector<int>&nums, int target)
    {
        int l = 0, r = nums.size() - 1;
        while(l <= r)
        {
            int mid = (l+r)/2;
            if(nums[mid]==target)return mid;
            if(target > nums[mid])
                l = mid + 1;
            else
                r = mid - 1;
        }
        return -1;
    }
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        int index = -1;
        vector<int>result;
        for(int i = 0; i < numbers.size(); i++)
        {
            index = binarySearch(numbers,target-numbers[i]);
            if(index != -1)
            {
                //还需要去除重复取值的情况
                if(i+1 == index + 1)
                    continue;
                result.push_back(i + 1);
                result.push_back(index + 1);
                break;
            }
        }
        //还得从小到大排列???
        sort(result.begin(), result.end());
        return result;
    }
};

leetcode151:反转单词顺序

【题目】

方法一:双指针

算法解析:

  • 倒序遍历字符串s,记录单词左右索引边界i,j
  • 每确定一个单词的边界,则将其添加至单词列表res;
  • 最终,将单词列表拼接为字符串,并返回。

复杂度:

  • **时间复杂度O(N)*其中N为字符串s的长度,线性遍历字符串
  • **空间复杂度O(N)*新建的字符串总长度<=N,占用O(N)大小的额外空间

数据结构与算法(一)_第10张图片

删除首位空格
形成单词数组
拼接单词数组即可
class Solution {
public:
    string reverseWords(string s) {
        string res;
        int n = s.size();
        if(n == 0) return res;
        int right = n-1;
        while(right >= 0)
        {
            //从后往前寻找第一个字符
            while(right >= 0 && s[right] == ' ')right--;
            if(right < 0) break;
            //从后往前寻找第一个空格
            int left = right;
            while(left >= 0 && s[left] != ' ') left--;
            //添加单词到结果
            res += s.substr(left + 1, right - left);
            res += ' ';

            //继续往前分割单词
            right = left;
        }
        //去除最后一个字符空格
        if(!res.empty()) res.pop_back();
        return res;          
    }
}; 

方法二:分割+倒序

利用”字符串分割",“列表倒序”的内置函数,可简便的实现本题的字符串反转

class Solution {
public:
    string reverseWords(string s) {
        stringstream iss(s);
        string ret="";
        string str;
        while(getline(iss,str,' '))
        {
            if(str!="")
            {
                if(ret=="")
                    ret=str;
                else
                    ret=str+" "+ret;
            }
        }
        return ret;
    }

leetcode125:验证回文串

【题目】

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写

说明:本题中,我们将空字符串定义为有效的回文串。

示例 1:

输入: “A man, a plan, a canal: Panama”
输出: true
示例 2:

输入: “race a car”
输出: false

【C++代码】

class Solution {
    //确定返回值:空,输入:字符串;功能:输入字符串,将字符串变为小写,去掉无用字符
	//确定返回值:字符串,输入:字符串;功能:输入字符串,将字符串变为小写,去掉无用字符
	string strTolowercase(string & str){
		string s;
		for (int i = 0; i < str.size(); i++){
			if ('A' <= str[i] && str[i] <= 'Z')
				//常识点一:字母由大写转成小写需要+32
				//大写英文字母:65-90;小写英文字母:97-122
				str[i] = str[i] + 32;
                //根据输入要求包括大小英文字母和数字所以需要特殊考虑输入数字的情况
			if (('a' <= str[i] && str[i] <= 'z') || ('0' <= str[i] && str[i] <= '9'))
				s.push_back(str[i]);
		}
		return s;
	}
public:
    bool isPalindrome(string s) {
        s = strTolowercase(s);
        //初始化数组
        int begin_index = 0;
        int end_index = s.size() - 1;
        //遍历整个字符串
        while(end_index > begin_index){
            if(s[begin_index] != s[end_index])
            {
                return false;
            }
            end_index--;
            begin_index++;
        }
        return true;
    }
};

【参考答案】

//时间复杂度:O(s);
//空间复杂度:O(1);
class Solution {
public:
    bool isPalindrome(string s) {
        int n = s.size();
        int left = 0, right = n - 1;
        while (left < right) {
            //isalnum[char c]是判断c是否是字母或者数字,如果是返回非零值,不是返回零
            while (left < right && !isalnum(s[left])) {
                ++left;
            }
            while (left < right && !isalnum(s[right])) {
                --right;
            }
            if (left < right) {
                //tolower(char c)如果c由对应的小写字母,返回小写字母否则保持不变
                if (tolower(s[left]) != tolower(s[right])) {
                    return false;
                }
                ++left;
                --right;
            }
        }
        return true;
    }
};

leetcode344:反转字符串

【题目】

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。(这句话一出就是要求你用双指针)

你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符

示例 1:

输入:[“h”,“e”,“l”,“l”,“o”]
输出:[“o”,“l”,“l”,“e”,“h”]
示例 2:

输入:[“H”,“a”,“n”,“n”,“a”,“h”]
输出:[“h”,“a”,“n”,“n”,“a”,“H”]

class Solution {
    //因为本题主要考察的就是逻辑加交换,所以尽量不要用原有的库函数
    void Swap(char& c1, char& c2){
        char temp_char = c1;
        c1 = c2;
        c2 = temp_char;
    }
public:
    void reverseString(vector<char>& s) {
        //初始化数组
        int begin_index = 0;
        int end_index = s.size() - 1;

        //遍历字符数组
        while(begin_index < end_index){
            Swap(s[begin_index], s[end_index]);
            //改变终止变量,否则会进入死循环
            begin_index++;
            end_index--;
        }
    }
};

class Solution {
public:
	void reverseString(vector<char>& s) {
		int l = 0, r = s.size() - 1;
		while (l < r) {
			swap(s[l], s[r]);
			l++; r--;
		}
	}
};
void main() {
	vector<char> s = { 'h','e','l','l','o' };
	Solution().reverseString(s);
	for (int i = 0; i < s.size(); i++) {
		cout << s[i] << " ";
	}
	system("pause");
	return;
}

leetcode345:反转字符串中的元音字母

【题目】

编写一个函数,以字符串作为输入,反转该字符串中的元音字母。

示例 1:

输入:“hello”
输出:“holle”
示例 2:

输入:“leetcode”
输出:“leotcede”

提示:

元音字母不包含字母 “y” 。

set的初始化,值得学习

class Solution {
public:
    string reverseVowels(string s) {
        //用set来存储元音集合
        unordered_set<char> Vowel_set = {'a', 'e','i','o','u','A','E','I','O','U'};
        //初始化两个指针
        int begin_index = 0;
        int end_index = s.size() - 1;

        //不断判断当前位置的字符是否存在元音集合中,有四种情况
        //值得注意的是从情况一到情况四都需要不停移动终止条件,否则会陷入死循环
        while(end_index > begin_index){
            //情况一:s[begin_index]是元音,s[end_index]是元音所以交换,注意的是如果交换完注意修改终止条件
            if((Vowel_set.find(s[begin_index]) != Vowel_set.end()) && (Vowel_set.find(s[end_index]) != Vowel_set.end()))
            {
                swap(s[begin_index], s[end_index]);
                begin_index++;
                end_index--;
            } 
            //情况二:s[begin_index]不是元音,s[end_index]是元音-》只需要将begin_index后移
            else if((Vowel_set.find(s[begin_index]) == Vowel_set.end()) && (Vowel_set.find(s[end_index]) != Vowel_set.end()))
                begin_index++;
            //情况三:s[begin_index]是元音,s[end_index]不是元音-》end_index前移
            else if((Vowel_set.find(s[begin_index]) != Vowel_set.end()) && (Vowel_set.find(s[end_index]) == Vowel_set.end()))
                end_index--;
            //情况四:s[begin_index]不是元音,s[end_index]不是元音-》两者都移动
            else
            {
                begin_index++;
                end_index--;
            }
        }
        
        return s;
    }
};

优化上述代码:就是把判断元音换成一个函数

【c++标准答案】

class Solution {
public:
    string reverseVowels(string s) {
        if(s.empty()) {
            return s;
        }
        int l = 0,r = s.size() - 1;
        while(l < r) {
            while (l < r && IsVowel(s[l]) == false) {
                l++;   
            } 
            while (l < r && IsVowel(s[r]) == false) {
                r--;
            }
            swap(s[l++], s[r--]);
        }
        return s;
    }

    bool IsVowel(char alphabet) {
        if (alphabet == 'a' || alphabet == 'A' || alphabet == 'e' || alphabet == 'E' || alphabet == 'i' || alphabet == 'I' 
        || alphabet == 'o' || alphabet == 'O' || alphabet == 'u' || alphabet == 'U') {
            return true;
        }  

        return false; 
    }    
};

leetcode11:盛最多水的容器

【题目】

给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水(输出)

说明:你不能倾斜容器。

img

输入:[1,8,6,2,5,4,8,3,7]
输出:49 
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。

【思路】

分析

我们先从题目中的示例开始,一步一步地解释双指针算法的过程。稍后再给出算法正确性的证明。

题目中的示例为:

[1, 8, 6, 2, 5, 4, 8, 3, 7]
^ ^
在初始时,左右指针分别指向数组的左右两端,它们可以容纳的水量为 \min(1, 7) * 8 = 8min(1,7)∗8=8。

此时我们需要移动一个指针。移动哪一个呢?直觉告诉我们,应该移动对应数字较小的那个指针(即此时的左指针)。这是因为,由于容纳的水量是由

两个指针指向的数字中较小值 * 指针之间的距离
两个指针指向的数字中较小值∗指针之间的距离

决定的。如果我们移动数字较大的那个指针,那么前者「两个指针指向的数字中较小值」不会增加,后者「指针之间的距离」会减小,那么这个乘积会减小。因此,我们移动数字较大的那个指针是不合理的。因此,我们移动 数字较小的那个指针。

有读者可能会产生疑问:我们可不可以同时移动两个指针? 先别急,我们先假设 总是移动数字较小的那个指针 的思路是正确的,在走完流程之后,我们再去进行证明。

所以,我们将左指针向右移动:

[1, 8, 6, 2, 5, 4, 8, 3, 7]
^ ^
此时可以容纳的水量为 \min(8, 7) * 7 = 49min(8,7)∗7=49。由于右指针对应的数字较小,我们移动右指针:

[1, 8, 6, 2, 5, 4, 8, 3, 7]
^ ^
此时可以容纳的水量为 \min(8, 3) * 6 = 18min(8,3)∗6=18。由于右指针对应的数字较小,我们移动右指针:

[1, 8, 6, 2, 5, 4, 8, 3, 7]
^ ^
此时可以容纳的水量为 \min(8, 8) * 5 = 40min(8,8)∗5=40。两指针对应的数字相同,我们可以任意移动一个,例如左指针:

[1, 8, 6, 2, 5, 4, 8, 3, 7]
^ ^
此时可以容纳的水量为 \min(6, 8) * 4 = 24min(6,8)∗4=24。由于左指针对应的数字较小,我们移动左指针,并且可以发现,在这之后左指针对应的数字总是较小,因此我们会一直移动左指针,直到两个指针重合。在这期间,对应的可以容纳的水量为:\min(2, 8) * 3 = 6min(2,8)∗3=6,\min(5, 8) * 2 = 10min(5,8)∗2=10,\min(4, 8) * 1 = 4min(4,8)∗1=4。

在我们移动指针的过程中,计算到的最多可以容纳的数量为49,即为最终的答案。

class Solution {
public:
    int maxArea(vector<int>& height) {
        //初始化
        int begin_index = 0;
        int end_index = height.size() - 1;
        int max_result = INT_MIN;
        int volume = 0;
        //异常处理如果hegith.size() == 2怎么办,如果size为2同样可以满足下面的逻辑条件
        while(end_index > begin_index
              //容量的左右最小的值*跨度
            volume = min(height[end_index], height[begin_index]) * (end_index - begin_index); 
              //选取max_result与volume最大的值赋值给max_result
            max_result = (max_result > volume) ? max_result:volume;
              //左右边界选取最小的值向中间靠拢,可能volume的值会比之前大
            if(height[begin_index] > height[end_index])
                end_index--;
            else
                begin_index++;
        }
        return max_result;
    }
};

leetcode977:有序数组的平方(方法二:归并排序值得学习还有与之对应的分而治之)

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1:

输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
示例 2:

输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

提示:

1 <= nums.length <= 104
-10^4 <= nums[i] <= 10^4
nums 已按 非递减顺序 排序

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> res(nums.size());
        for(int i = 0 ; i < nums.size(); i++)
        {
            res[i] = nums[i] * nums[i];
        }
        sort(res.begin(), res.end());
        return res;
    }
};


上述方法ing没有使用数组nums已经按照升序排序这个条件,显然,如果数组nums中的所有数都是非负数,那么将每个数平方后,数组仍然保持升序;如果数组nums中所有数都是负数,那么将每个数平方后将保持降序。

【思路】

如果我们能够找到数组nums中负数与非负数的分届线,那么就可以使用类似的归并排序的方法。具体,我们设置neg为数组nums中非负数与负数的分解线,也就是说nums[0]到nums[neg]均为负数,而nums[neg+1]到nums[n-1]均为非负数。所以当我们平方的时候,从nums[0]到nums[neg]是递减的,nums[ngt+1]到nums[n-1]单调递增 。由于我们得到两个已经有序的子数组,因此就可以使用归并的方法进行排序。

具体实现:使用两个指针分别指向位置neg与neg+1,每次比较两个指针对应的数,选择比较小的放入到答案数组并移动指针。当某一指针移动到边界时,将另一指针还未遍历的数依此放入答案。------》这种思想在链表中也出现过

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int len = nums.size();
        int negative = -1;
        //寻找分界点,这个分解点左边是负数[0,negative],右边是正数[negative+1, len-1]
        for(int i = 0; i < n; i++)
        {
            if(nums[i] < 0)
            {
                negative = i;
            }
            else
            {
                break;
            }
        }

        vector<int> res;
        int i = negative, j = negative + 1;
        while(i >= 0 || j < len)
        {
            if(i < 0)       //如果i对应的数组已经没有了,剩下的只能加入j对应的数组
            {
                res.push_back(nums[j] * nums[j]);
                j++;
            }
            else if(j == n) //如果j对应的数组已经没有了,剩下的就只能加入i对应的数组
            {
                res.push_back(nums[i] * nums[i]);
                i--;
            }
            else if(nums[i] * num [i] > nums[j] * nums[j])
            {
                res.push_back(nums[j] * nums[j]);
                j++;
            }
            else            //如果nums[i] * num [i] <= nums[j] * nums[j]
            {
                res.push_back(nums[i] * nums[i]);
                i--;
            }
        }
        return res;
    }
};

leetcode844:比较含退格的字符串

给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。

注意:如果对空文本输入退格字符,文本继续为空。

示例 1:

输入:S = “ab#c”, T = “ad#c”
输出:true
解释:S 和 T 都会变成 “ac”。
示例 2:

输入:S = “ab##”, T = “c#d#”
输出:true
解释:S 和 T 都会变成 “”。
示例 3:

输入:S = “a##c”, T = “#a#c”
输出:true
解释:S 和 T 都会变成 “c”。
示例 4:

输入:S = “a#c”, T = “b”
输出:false
解释:S 会变成 “c”,但 T 仍然是 “b”。

提示:

1 <= S.length <= 200
1 <= T.length <= 200
S 和 T 只含有小写字母以及字符 ‘#’。

进阶:

你可以用 O(N) 的时间复杂度和 O(1) 的空间复杂度解决该问题吗?(双指针)

class Solution {
    string build(string str)
    {
        string ret;
        for(char ch : str)
        {
            if(ch != '#')
            {
                ret.push_back(ch);
            }
            else if(!ret.empty())
            {
                ret.pop_back();
            }
        }
        return ret;
    }
public:
    bool backspaceCompare(string s, string t) {
        return build(s) == build(t);
    }
};

进阶:

class Solution {
public:
    bool backspaceCompare(string s, string t) {
        int i = s.size() - 1, j = t.size() - 1;
        int skipS = 0, skipT = 0;
        while(i >= 0 || j >= 0)
        {
            while(i >= 0)
            {
                if(s[i] == '#')
                {
                    skipS++;
                    i--;
                }
                else if(skipS > 0)
                {
                    skipS--;
                    i--;
                }
                else//这个对应的是什么情况 s[i] != #并且skips==0
                {
                    break;
                }
            }

            while(j >= 0)
            {
                if(t[j] == '#')
                {
                    skipT++;
                    j--;
                }
                else if(skipT > 0)
                {
                    skipT--;
                    j--;
                }
                else
                {
                    break;
                }
            }

            if(i >= 0 && j >= 0)
            {
                if(s[i] != t[j])
                    return false;
            }
            else//包含三种情况:1。i小于零j小于零;2。i小于零j大于零;3。i大于零j小于零。对于2与3两种情况返回false对于1这种情况返回true
            {
                if(i >= 0 || j >= 0)
                {
                    return false;//这对应的是那种情况????
                }
            }

            i--;
            j--;
        } 
        return true;
    }
};

滑动窗口

leetcode209:长度最小的子数组

【题目】

给定一个含有 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

class Solution {
	//时间复杂度:O(n)
	//空间复杂度:O(1)
public:
	int minSubArrayLen(int target, vector<int>& nums) {
		int l = 0, r = 0;//nums[l,,,r]为我们的滑动窗口
		int sum = 0;
		int res = INT_MAX;//int_max可以换成这个在limits.h头文件中
		//数组只要由方框就要放置越界
		while (l < nums.size()) {
			if (r  < nums.size() && sum < target) {
				sum += nums[r];//方括号取值要考虑数组越界的问题r + 1 < nums.size()
                r++;
			}
			else {
				sum -= nums[l];//l不需要管因为while里的条件
				l++;
			}
            //上面两个条件已经可以修改l或者r改变循环变量了,所以在下面的if里不需要再改变循环变量
			if (sum >= target) {
				res = min(res, r - l);
			}
		}
        //异常情况处理
        return (res==INT_MAX)? 0 : res;
	}
};

方法二:双指针+set容器

class Solution {
public:
    /**
     * 
     * @param arr int整型vector the array
     * @return int整型
     */
    int maxLength(vector<int>& arr) {
        // write code here
        int right = 0, left = 0;
        int res = -1;
        unordered_set<int> set;
        while(right < arr.size())
        {
            if(set.find(arr[right]) == set.end())
            {
                set.insert(arr[right]);
                right++;
            }
            else
            {
                res = res > (right - left)?res:right-left;
                while(left <= right)
                {
                    if(arr[left] != arr[right])
                    {
                        set.erase(arr[left]);
                        left++;
                    }
                    else
                    {
                        set.erase(arr[left]);
                        left++;
                        break;
                    }
                }
            }
        }
        res = res > (right - left )? res : right-left;
        return res;
    }
};

leetcode3:无重复字符的最长子串

【题目】

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度

示例 1:

输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:

输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:

输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
示例 4:

输入: s = “”
输出: 0

【代码】

//确定字符集:该字符集是由英文、数字、符号、空格组成不超过256个所以使用256个数组表示
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        if(s.size() == 0) return 0;
        vector<int> freq(256);
        int l = 0, r = 0;//选取的滑窗[l,r)
        int res = INT_MIN;
        while(l < s.size()){
            //让r向右走
            if(r < s.size() && freq[s[r]] == 0){
                freq[s[r]]++;
                r++;
                res = max(res,r - l );//一定注意边界条件//这个位置小写对
            }else{
                freq[s[l]] = 0;
                l++;
            }
          //  res = max(res,r - l );//一定注意边界条件//这个位置小写对
        }
        return res;
    }
};



方法二:用set来做
   class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int left = 0 ,right = 0;
        int len = s.size();
        unordered_set<char> Hash_set;
        int res = 0;
        while(right < len)
        {
            if(Hash_set.find(s[right]) == Hash_set.end())//如果没有找到就添加s[right]
            {
                Hash_set.insert(s[right]);
                right++;
                res = max(res, right - left);
            }
            else
            {
                Hash_set.erase(s[left]);
                left++;
            }
        }
        return res;
    }
}; 

【方法二:动态规划+遍历】

分析

长度为N的字符串共有(1+N)*N/2个子字符串(时间复杂度为O(N * N),判断长度为N的字符串是否有重复字符的复杂度为O(N),因此本题使用暴力法解决的时间复杂度为O(N * N * N)

  1. 确定数组下标及意义

    dp[j]:代表以字符s[j]结尾的“最长不重复子字符串”的长度为dp[j]。

  2. 确定状态转移方程

    固定右边界j,设字符s[j]左边界距离最近的相同字符为s[i],即s[i] = s[j]。

    • 当 i < 0,即s[j]左边无相同字符,则dp[j] = dp[j-1] + 1;
    • dp[j-1] < j - i, 说明字符s[i]在子字符串dp[j-1]区间之外,dp[j] = dp[j-1]+1;
    • dp[j-1] >= j-i;说明字符s[i]在子字符串dp[j-1]区间之中,则dp[j]的左边界由s[i]决定,即dp[j] = j - i;

    当i < 0时,由于dp[j-1] <= j恒成立,因而dp[j-1] < j-i恒成立,因此分治1与2可以被合并。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OxEJd3FP-1626103289590)(C:\Users\mubao\AppData\Roaming\Typora\typora-user-images\image-20210524171448797.png)]

  3. 初始状态初始化

    由递推公式可以看出,遍历顺序时从左向右的。初始化为零就可以了

  4. 确定遍历顺序

    数据结构与算法(一)_第11张图片

空间复杂度优化:

  • 由于返回值是取dp列表最大值,因此可借助变量tmp存储dp[j],变量res每轮更新最大值

  • 此优化可节省dp列表使用的O(N)大小的额外空间。

时间复杂度:O(N*N)
空间复杂度:O(1),用的tmp变量存储dp数组的值

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int res = 0, tmp = 0;//tmp当前时刻最长的长度
        for(int j = 0; j < s.size(); j++)
        {
            int i = j - 1;
            //是从j-1的位置开始找与s[j]相同的字符
            while(i >= 0 && s[i] != s[j]) i--;
            tmp = tmp < j - i ? tmp + 1 : j - i;//dp[j-1]->dp[j],这是递推公式
            res = max(res, tmp);//保存最长的那个字符串的个数
        }
        return res;
    }
};

【方法二:动态规划+哈希表】

  • 哈希表统计遍历字符串s时,使用哈希表dic统计各字符最后一次出现的索引位置。
  • 左边界i的获取方式:遍历道s[j]时,可通过访问哈希表dic[s[j]]获取最近的相同字符的索引i。

复杂度分析:

  • 时间复杂度:O(N),其中N为字符串长度,动态规划需要遍历计算dp列表
  • 空间复杂度:O(1),字符的ASCII码范围为0~127,哈希表dic最多使用O(128)=O(1)大小的额外空间,
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_map<char, int> Hash_map;
        int res = 0, tmp = 0, i = -1;
        //注意i的初始化是-1
        for(int j = 0; j < s.size(); j++)
        {
            if(Hash_map.find(s[j]) != Hash_map.end())
                i = Hash_map[s[j]];
            Hash_map[s[j]] = j;
            tmp = tmp < j - i ? tmp + 1 : j - i;
            res = max(res, tmp);
        }
        return res;
    }
};

【方法三:双指针+哈希表】

  • **哈希表dic统计:**指针j遍历字符s,哈希表统计字符s[j]最后一次出现的索引

  • **更新左指针:**根据上轮左指针i和dic[s[j]],每轮更新左边界i,保证区间[i+1,j]内无重复字符且最大

    i = max(dic[s[j]], i);
    
  • 更新结果res:取上轮res和本轮指针区间[i+1, j]的宽度(j-i)中的最大值。

    res = max(res, j - i);
    

复杂度分析:

  • 时间复杂度O(N):其中N为字符串长度,动态规划需要遍历计算dp列表

  • **空间复杂度O(1)*字符的ASCII码范围为0~127,哈希表dic最多使用O(128)=O(1)的空间复杂度:

    class Solution {
    public:
        int lengthOfLongestSubstring(string s) {
            unordered_map<char, int> Hash_map;
            int i = -1, res = 0;
            for(int j = 0; j < s.size(); j++)
            {
                if(Hash_map.find(s[j]) != Hash_map.end())
                {
                    i = max(i, Hash_map[s[j]]);
                }
                Hash_map[s[j]] = j;
                res = max(res, j - i);
            }
            return res;
        }
    };
    

leetcode76:最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
示例 2:

输入:s = “a”, t = “a”
输出:“a”

提示:

1 <= s.length, t.length <= 105
s 和 t 由英文字母组成

【说明】

如果S中不存在这样的子串,则返回空字符串”“

如果S中存在这样的子串,我们保证它时唯一的答案

子串强调了连续性;

T中可能包含重复字符

如何判断S的子串包含了T中的的所有字符?

参考做法:

分别统计S的子串与T中每个字符出现的次数,然后逐个对比。
class Solution {
public:
    //哈希表
    unordered_map <char, int> ori, cnt;
    //判断cnt中字符是否小于原始数据中的字符的数量
    bool check() 
    {
        for (const auto &p: ori) 
        {
            if (cnt[p.first] < p.second) 
            {
                return false;
            }
        }
        return true;
    }

    string minWindow(string s, string t) 
    {

        for (const auto &c: t) 
        {
            //用哈希表统计原始字符串t中字符的个数
            ++ori[c];
        }
        //区间定义为[l, r)
        int l = 0, r = 0;
        int len = INT_MAX, ansL = -1;//L的左边界下标时不可能为负数的

        while (r < s.size()) 
        {
            //在ori数组中找s[r]字符,如果找到了,cnt[s[r]]++,s[r]字符数量加1
            if (ori.find(s[r]) != ori.end()) 
            {
                cnt[s[r]]++;
            }
            r++;//r++不能放在后面,因为下面的代码还要用用r
            while (check() && l <= r) 
            {
                if (r - l  < len) 
                {
                    len = r - l;
                    ansL = l;
                }
                if (ori.find(s[l]) != ori.end()) 
                {
                    cnt[s[l]]--;
                }
                l++;
            }  
        }
        return ansL == -1 ? "" : s.substr(ansL, len);
    }
};

leetcode424:替换后的最长重复字符

给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。

注意:字符串长度 和 k 不会超过 10^4。

示例 1:

输入:s = “ABAB”, k = 2
输出:4
解释:用两个’A’替换为两个’B’,反之亦然。
示例 2:

输入:s = “AABABBA”, k = 1
输出:4
解释:
将中间的一个’A’替换为’B’,字符串变为 “AABBBBA”。
子串 “BBBB” 有最长重复字母, 答案为 4。

[思路]

数据结构与算法(一)_第12张图片

我们移动指针,此时 maxsam = 4 ,k = 2 我们的 maxsame + k < right - left + 1。则说明此时不能完全替换掉不同的字母,得到相同字母的子串。我们可以看图,我们此时有 4 个 B,为窗口内最多的字母,有另外三个不同字母,A,A,C 但是此时我们的 k = 2,最多能替换两个,则需要移动我们的 left 指针缩小窗口。到重点就是 maxsame + k < right - left + 1

class Solution {
public:
    int characterReplacement(string s, int k) {
        vector<int> num(26);
        unordered_map<char, int> Freq;
        int n = s.size();
        int left = 0, right = 0, maxsame = 0;
        while(right < n)
        {
            //num[s[right] - 'A']++;
            Freq[s[right]]++;
            //为什么没有想到最大相同数量呢????通过比较得到最大相同数量,当滑窗内个数大于最大相同个数+k时,滑窗左边界左移
            //maxsame = max(maxsame, num[s[right] - 'A']);
            maxsame = max(maxsame, Freq[s[right]]);
            //当right - left + 1  > k + maxsame时,说明此时不能完全替换掉不同的字母,得到相同字母的子串
            if(right - left + 1  > k + maxsame)
            {
                //num[s[left] - 'A']--;
                Freq[s[left]]--;
                left++;
            }
            right++;
        }
        //注意返回值的大小
        //为什么结果是right-left,这是因为maxsame如果是最大不动时,right-left已经时固定的滑动窗口了
        return right - left;
    }
};

leetcode438:找到字符串中所有字母的异位词(固定长度的滑动窗口)leetcode567:字符串的排列与该题思路相同

给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。

字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。

说明:

字母异位词指字母相同,但排列不同的字符串。
不考虑答案输出的顺序。
示例 1:

输入:
s: “cbaebabacd” p: “abc”

输出:
[0, 6]

解释:
起始索引等于 0 的子串是 “cba”, 它是 “abc” 的字母异位词。
起始索引等于 6 的子串是 “bac”, 它是 “abc” 的字母异位词。
示例 2:

输入:
s: “abab” p: “ab”

输出:
[0, 1, 2]

解释:
起始索引等于 0 的子串是 “ab”, 它是 “ab” 的字母异位词。
起始索引等于 1 的子串是 “ba”, 它是 “ab” 的字母异位词。
起始索引等于 2 的子串是 “ab”, 它是 “ab” 的字母异位词。

//固定长度滑动窗口
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        vector<int>ans;
        if(p.size() > s.size()||s.size() == 0) 
			return ans;
        vector<int>need(26);
        vector<int>windows(26);
        for(char a:p) 
			need[a-'a']++;
        //这里的window的下标[0, p.size() - 2],因为这样所以后面的采用windows[s[r++]]++;否则会出错
        for(int i=0;i < p.size()-1;i++) 
			windows[s[i]-'a']++;
        
        int l = 0,r = p.size()-1;//窗口是[l,r]
        while(r < s.size())
        {
            windows[s[r]-'a']++;
            r++;
            if(windows==need) ans.push_back(l);
            windows[s[l]-'a']--;
            l++;
        }
        return ans;
    }
};

leetcode930:和相同的二元子数组

给你一个二元数组 nums ,和一个整数 goal ,请你统计并返回有多少个和为 goal 的 非空 子数组。

子数组 是数组的一段连续部分。

 

示例 1:

输入:nums = [1,0,1,0,1], goal = 2
输出:4
解释:
有 4 个满足题目要求的子数组:[1,0,1]、[1,0,1,0]、[0,1,0,1]、[1,0,1]
示例 2:

输入:nums = [0,0,0,0,0], goal = 0
输出:15
 

提示:

1 <= nums.length <= 3 * 104
nums[i] 不是 0 就是 1
0 <= goal <= nums.length

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/binary-subarrays-with-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
class Solution {
public:
    int numSubarraysWithSum(vector<int>& nums, int goal) {
        unordered_map<int,int> mp;
        int sum = 0;
        int res = 0;
        for(auto n : nums)
        {   
            mp[sum]++;
            sum += n;
            res += mp[sum - goal];
        }
        return res;
    }
};

//方法二:
/*
时间复杂度: O(n)
空间复杂度:O(1)
*/
class Solution {
public:
    int numSubarraysWithSum(vector<int>& nums, int g) {
        int l1 = 0, l2 = 0, r = 0;
        int sum1 = 0, sum2 = 0;
        int res = 0;
        while(r < nums.size())
        {
            //缩小窗口sum1使窗口内的和为小于等于g
            sum1 += nums[r];
            while(l1 <= r && sum1 > g)
            {
                sum1 -= nums[l1];
                l1++;
            }
            //缩小窗口sum2使窗口内的和为小于g
            sum2 += nums[r];
            while(l2 <= r && sum2 >=g )
            {
                sum2 -= nums[l2];
                l2++;
            }
           //两者一减就是此时的非空子数组的个数
            res += l2 - l1;
            r++;
        }
        return res;
    }
};

leetcode5739:最高频元素的频数

元素的 频数 是该元素在一个数组中出现的次数。

给你一个整数数组 nums 和一个整数 k 。在一步操作中,你可以选择 nums 的一个下标,并将该下标对应元素的值增加 1 。

执行最多 k 次操作后,返回数组中最高频元素的 最大可能频数 。

 

示例 1:

输入:nums = [1,2,4], k = 5
输出:3
解释:对第一个元素执行 3 次递增操作,对第二个元素执 2 次递增操作,此时 nums = [4,4,4] 。
4 是数组中最高频元素,频数是 3 。
示例 2:

输入:nums = [1,4,8,13], k = 5
输出:2
解释:存在多种最优解决方案:
- 对第一个元素执行 3 次递增操作,此时 nums = [4,4,8,13] 。4 是数组中最高频元素,频数是 2 。
- 对第二个元素执行 4 次递增操作,此时 nums = [1,8,8,13] 。8 是数组中最高频元素,频数是 2 。
- 对第三个元素执行 5 次递增操作,此时 nums = [1,4,13,13] 。13 是数组中最高频元素,频数是 2 。
示例 3:

输入:nums = [3,9,6], k = 2
输出:1
 

提示:

1 <= nums.length <= 10^5
1 <= nums[i] <= 10^5
1 <= k <= 10^5

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/frequency-of-the-most-frequent-element
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

数据结构与算法(一)_第13张图片

class Solution {
    /*
    //功能函数:返回diff中存在和小于等于k的最大连续数组的值
    //这种方法是不对的应为
    //原数组为 :1 1 1 4 5 6 7 8 9 9
    //差值数组:0 0 3 1 1 1 1 1 0
    //不能向上兼容
    int max_index(vector& diff, int k)
    {
        
        int len = diff.size();
        if(len == 1 && diff[0] <= k)
            return 1;
        else if(len == 1 && diff[0] > k)
            return 0;
        
        int res = 0;
        int left = 0; 
        int sum = 0;
        int right = left;
        int result = INT_MIN;
        while(right < len)
        {
            sum = sum + diff[right];
            if(sum <= k)
            {
                res++;
                result = result > res ? result : res;
                right++;
            }
            else
            {
                res = 1;
                left = right;
                sum = diff[left];
                right++;
            }
        }
        return result == INT_MIN ? 0 : result;
    }
public:
    int maxFrequency(vector& nums, int k) {
        vector diff;
        int len = nums.size();
        if(len == 1)
            return 1;
        sort(nums.begin(), nums.end());
        for(int i = 1; i < len; i++)
        {
            diff.push_back(nums[i] - nums[i-1]);
        }
        //已知差值数组了,怎么找到小于k的最长长度
        int a = max_index(diff, k);
        return a + 1;
    }
    */

    public:
    int maxFrequency(vector<int>& nums, int k) {
        sort(nums.begin(), nums.end());

        int len = nums.size();
        int left = 0;
        //根据输入值来判断数据的类型,避免可能的越界
        long long sum = 0;
        //因为数据最大为10^5,多个数相加可能会越界
        int max_len = 0;

        for(int right = 0; right < len; right++)
        {
            //滑动窗口的右边界
            sum += nums[right];
            while((long long )nums[right] * (right - left + 1) - sum > k)
            {
                //滑动窗口的左边界
                sum -= nums[left];
                left++;
            }
            max_len = max(max_len , right - left + 1);
        }
        return max_len;
    }
};

leetcode239:滑动窗口最大值

【题目】

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7] 
解释: 

  滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
 

提示:

你可以假设 k 总是有效的,在输入数组不为空的情况下,1 ≤ k ≤ 输入数组的大小。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/hua-dong-chuang-kou-de-zui-da-zhi-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

【解题思路】

设窗口区间为[i,j],最大值为xj。当窗口向前移动一格,则区间变为[i+1, j+1],即添加了nums[j+1],删除了nums[i]。若只向窗口[i,j]右边添加数字nums[j+1],则新窗口最大值可以通过一次对比使用O(1)时间得到,即:xj+1 = max(xj, nums[j+1])

而由于删除的nums[i]可能恰好是窗口内唯一的最大值xj,因此不能通过以上方法计算xj+1,而必须使用O(j-i)时间,遍历整个窗口区间获取最大值,即:

xj+1 = max(nums[i+1],…,nums[j+1])

根据以上分析,可得暴力法的时间复杂度为O((n-k+1)*k)约等于O(nk).

  • 设数组nums的长度为n,则共有((n-k+1))个窗口
  • 获取每个窗口最大值需要线性遍历,时间复杂度为O(K)

数据结构与算法(一)_第14张图片

回想剑指Offer30包含min函数的栈,其使用单调栈实现随意入栈、出栈情况下的O(1)时间获取栈内最小值。不同点在于“出栈操作”删除的是“列表尾部元素”,而“窗口滑动”删除的是“列表首部元素”

窗口对应的是双端队列,本题使用单调队列即可解决以上问题,遍历数组时,每轮保证单调队列deque:

  1. deque内仅包含窗口的元素====》每轮窗口滑动移除了元素nums[i-1],需将deque内的对应元素一起删除。
  2. deque内的元素非严格递减==》每轮窗口滑动添加了元素nums[j+1],需将deque内所有

算法流程:

  1. 初始化:双端队列deque,结果列表res,数组长度n
  2. 滑动窗口左边界范围i属于[i-k,n-k]右边节范围j属于[0,n-1];
    • 若i>0且队首元素deque[0]=被删除元素nums[i-1]:则队首元素出队
    • 删除deque内所有
    • 将nums[j]添加至deque尾部
    • 若已形成窗口(即i >= 0):将窗口最大值(即队首元素deque[0])添加至列表res
  3. **返回值:**返回结果列表
时间复杂度:O(n),其中n为数组nums长度,线性遍历nums占用O(n);每个元素最多仅入队和出队一次,因此单调队列deque占用O(2n)
空间复杂度:O(k),双端队列deque中最多同时存储k个元素即窗口大小
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        //边界处理
        if(nums.size() == 0 || k == 0) return  {};
        deque<int> dq;
        vector<int> res;
        //用一个循环解决问题
        for(int i = 1 - k, j = 0; j < nums.size(); i++, j++)
        {
            //删除deque中对应的nums[i-1]
            if(i > 0 && dq.front() == nums[i-1]) 
                dq.pop_front();
            while(!dq.empty() && dq.back() < nums[j])
                dq.pop_back();
            dq.push_back(nums[j]);
            if(i >= 0)
                res.push_back(dq.front());
        }
        return res;
    }
};

剑指Offer59—II:队列的最大值(单调双向队列)

【题目】

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

输入: 
["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]
示例 2:

输入: 
["MaxQueue","pop_front","max_value"]
[[],[],[]]
输出: [null,-1,-1]
 

限制:

1 <= push_back,pop_front,max_value的总操作数 <= 10000
1 <= value <= 10^5

解题思路

对于普通队列,入队push_back(),出队pop_front()的时间复杂度均为O(1);而查找最大值max_value()的O(1) 时间复杂度。

假设队列中存储N个元素,从中获取最大值需要遍历队列,时间复杂度为O(N),但从算法上无法优化

所以最直观的相法就是维护一个最大值变量,在y㢝入队时更新此变量即可;但当最大值出队后,并无法确定下一个次最大值,因此不可行。

数据结构与算法(一)_第15张图片

考虑利用数据结构来实现,即经常使用的“空间换时间”,如下图,构建一个递减列表来保存队列中所有递减元素,递减链表随着入队和出队操作实现实时更新,这样队列最大元素就是中对应递减列表的首元素,实现了获取最大值O(1)时间复杂度.。

数据结构与算法(一)_第16张图片

为了实现递减列表,需要使用双向队列,假设队列已经有若干元素:

  1. 当执行入队push_back()时:若入队要给比队列某些元素更大的数字x,则为了保持此列表递减,需要将双向队列尾部所有小于x的元素弹出。
  2. 当执行出队pop_front()时:若出队的元素时最大元素,则双向队列需要同时将首元素出队,以保持队列和双向队列的一致性。
使用双向队列原因:维护递减列表需要元素队首弹出、队尾插入、队尾弹出操作皆为 O(1) 时间复杂度。

函数设计:

初始化队列queue,双向队列deque;

最大值max_value():

  • 当双向队列deque为空,则返回-1;
  • 否则,返回deque首元素;

入队push_back()

  • 将元素value入队queue;
  • 将双向队列中队尾所有小于value的元素弹出(以保持deque非单调递减),并将value如队deque;

出队pop_front():

  1. 若队列queue为空,则直接返回-1;
  2. 否则,将queue首元素出队
  3. 若deque首元素和queue首元素相等,则将deque首元素出队(以保持两队元素一致)
设计双向队列为单调不增的原因:若队列queue中存在两个值相同的最大元素,此时queue和deque同时弹出一个最大元素,而queue中还有一个此最大元素;即采用单调递减将导致两队列中的元素不一致。
时间复杂度:O(1):max_value(),push_back(),pop_frong()方法均摊时间复杂度为O(1);
空间复杂度:O(N),当元素个数为N时,最差情况下deque中保存N个元素,使用O(N)的额外空间。
class MaxQueue {
    queue<int> que;
    deque<int> deq;
public:
    MaxQueue() { }
     int max_value() {
        return deq.empty()? -1 : deq.front();
    }
    void push_back(int value) {
        que.push(value);
        while(!deq.empty() && deq.back() < value)
            deq.pop_back();
        deq.push_back(value);
    }
    int pop_front() {
        if(que.empty()) return -1;
        int val = que.front();
        if(val == deq.front())
            deq.pop_front();
        que.pop();
        return val;
    }
};

/**
 * Your MaxQueue object will be instantiated and called as such:
 * MaxQueue* obj = new MaxQueue();
 * int param_1 = obj->max_value();
 * obj->push_back(value);
 * int param_3 = obj->pop_front();
 */

leetcode5740:所有元音按顺序排布的最长子字符串

当一个字符串满足如下条件时,我们称它是 美丽的 :

所有 5 个英文元音字母('a' ,'e' ,'i' ,'o' ,'u')都必须 至少 出现一次。
这些元音字母的顺序都必须按照 字典序 升序排布(也就是说所有的 'a' 都在 'e' 前面,所有的 'e' 都在 'i' 前面,以此类推)
比方说,字符串 "aeiou" 和 "aaaaaaeiiiioou" 都是 美丽的 ,但是 "uaeio" ,"aeoiu" 和 "aaaeeeooo" 不是美丽的 。

给你一个只包含英文元音字母的字符串 word ,请你返回 word 中 最长美丽子字符串的长度 。如果不存在这样的子字符串,请返回 0 。

子字符串 是字符串中一个连续的字符序列。

 

示例 1:

输入:word = "aeiaaioaaaaeiiiiouuuooaauuaeiu"
输出:13
解释:最长子字符串是 "aaaaeiiiiouuu" ,长度为 13 。
示例 2:

输入:word = "aeeeiiiioooauuuaeiou"
输出:5
解释:最长子字符串是 "aeiou" ,长度为 5 。
示例 3:

输入:word = "a"
输出:0
解释:没有美丽子字符串,所以返回 0 。
 

提示:

1 <= word.length <= 5 * 105
word 只包含字符 'a','e','i','o' 和 'u' 。

class Solution {
public:
    int longestBeautifulSubstring(string word) {
        //定义一个窗口,这个滑动窗口不一定是只用双指针,也可以是一个数组容器
        vector<char> window; 
        //cnt用于验证整个字符串是否由五个元音字符组成
        unordered_set<char> cnt;
        int res = 0;
        int left = 0, right = 0;
        while (right < word.size()) {
            //窗口右扩
            if (window.empty() || word[right] >= window.back()) {//容器中.back是最后一个插入的元素
                window.push_back(word[right]);
                cnt.insert(word[right]);
                // cout << "right: " << right << " win: " << window.size() << " cnt: " << cnt.size()<
                //满足条件,记录答案
                if (cnt.size() == 5) {
                    res = max(res, (int)window.size());
                }
            }
            //窗口左侧收缩
            else {
                // cout << "clear at right= " << right << endl; 
                window.clear();
                cnt.clear();
                left = right;
                window.push_back(word[left]);
                cnt.insert(word[left]);
            }
            right++;
        }
        return res;
    }
};


leetcode904:水果成篮

在一排树中,第 i 棵树产生 tree[i] 型的水果。
你可以从你选择的任何树开始,然后重复执行以下步骤:

把这棵树上的水果放进你的篮子里。如果你做不到,就停下来。
移动到当前树右侧的下一棵树。如果右边没有树,就停下来。
请注意,在选择一颗树后,你没有任何选择:你必须执行步骤 1,然后执行步骤 2,然后返回步骤 1,然后执行步骤 2,依此类推,直至停止。

你有两个篮子,每个篮子可以携带任何数量的水果,但你希望每个篮子只携带一种类型的水果。

用这个程序你能收集的水果树的最大总量是多少?

示例 1:

输入:[1,2,1]
输出:3
解释:我们可以收集 [1,2,1]。
示例 2:

输入:[0,1,2,2]
输出:3
解释:我们可以收集 [1,2,2]
如果我们从第一棵树开始,我们将只能收集到 [0, 1]。
示例 3:

输入:[1,2,3,2,2]
输出:4
解释:我们可以收集 [2,3,2,2]
如果我们从第一棵树开始,我们将只能收集到 [1, 2]。
示例 4:

输入:[3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:我们可以收集 [1,2,1,1,2]
如果我们从第一棵树或第八棵树开始,我们将只能收集到 4 棵水果树。

提示:

1 <= tree.length <= 40000
0 <= tree[i] < tree.length

class Solution {
public:
	int totalFruit(vector<int>& tree) {
		unordered_map<int, int> Hash_map;//key:tree[i],value:tree[i]对应的个数
		int left = 0, right = 0;
		int len = tree.size();
		int res = 0;
		while (left < len)
		{
			if (right < len && Hash_map.size() <= 2)
			{
				
				Hash_map[tree[right]]++;
				right++;
                //因为每次是先添加再计算,所以就容易多填。例如数据[]
                //3 3 3 1 2 1 1 2 3 3 4
                //|       |
                //left    right
                //此时Hash_map里有了三个值分别为3,1,2,此时不应该进行操作直接continue
				if (Hash_map.size() == 3)//为什么可以用map.size就是因为删掉left时,如果为零直接从map容器删除所以value=0并不占size的位置
					continue;
				res = max(res, right - left);
			}
			else
			{
				Hash_map[tree[left]]--;
				if (Hash_map[tree[left]] == 0)
					Hash_map.erase(tree[left]);
				left++;
			}
		}
		return res;
	}
};
class Solution {
public:
    int totalFruit(vector<int>& tree) {
        unordered_map<int, int> window;
        const int k = 2;
        int res = 0;
        int right = 0, left = 0;
        while(right < tree.size()) 
        {
            window[tree[right]]++;
            while (window.size() > k) 
            {
                window[tree[left]]--;
                if (window[tree[left]] == 0) 
                {
                    window.erase(tree[left]);
                }
                left++;
            }
            right++;
            res = max(res, right - left);
            /*
            right的位置不同,res 的表达式不相同
            res = max(res, right - left + 1);
            right ++;
            */
        }

        return res;
    }
};

leetcode252:会议室

给定一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,请你判断一个人是否能够参加这里面的全部会议。

示例 1:

输入:intervals = [[0,30],[5,10],[15,20]]
输出:false
示例 2:

输入:intervals = [[7,10],[2,4]]
输出:true

提示:

0 <= intervals.length <= 10^4
intervals[i].length == 2
0 <= starti < endi <= 10^6

class Solution {
    static bool cmp(vector<int> v1, vector<int> v2)
    {
        return v1[0] < v2[0];
    }
public:
    bool canAttendMeetings(vector<vector<int>>& intervals) {
        vector<int> begin_time;
        vector<int> end_time;
        int len = intervals.size();
        if(len == 1)
            return true;
        sort(intervals.begin(), intervals.end(), cmp);
        for(int i = 0; i < len; i++)
        {
            begin_time.push_back(intervals[i][0]);
            end_time.push_back(intervals[i][1]);
        }
        for(int i = 1; i < len; i++)
        {
            if( begin_time[i] < end_time[i-1] )
                return false;
        }
        return true;
    }
};

leetcode253:会议室II

给你一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,为避免会议冲突,同时要考虑充分利用会议室资源,请你计算至少需要多少间会议室,才能满足这些会议安排。

示例 1:

输入:intervals = [[0,30],[5,10],[15,20]]
输出:2
示例 2:

输入:intervals = [[7,10],[2,4]]
输出:1

提示:

1 <= intervals.length <= 10^4
0 <= starti < endi <= 10^6

class Solution {
public:
    int minMeetingRooms(vector<vector<int>>& intervals) {
        int len = intervals.size();
        assert(len >= 1);
        if( len == 1)
            return 1;
        //根据会议时间开始时间排序
        sort(intervals.begin(), intervals.end());
        priority_queue<int, vector<int>, greater<int>> rooms;
        for(auto & range : intervals)
        {
            const int& start_time = range[0];
            const int& end_time = range[1];
            if(rooms.empty())
            {
                rooms.push(end_time);               //以第一个会议结束时间来建立初始堆
                continue;
            }
            else if( rooms.top() <= start_time)     //有空余房间
            {
                rooms.pop();
                rooms.push(end_time);
            }
            else                                    //没有空余房间
            {
                rooms.push(end_time);
            }
        }
        return rooms.size();
    }
};

典型差分:上下车、上下飞机、上下台阶

leetcode379:区间加法

假设你有一个长度为 n 的数组,初始情况下所有的数字均为 0,你将会被给出 k 个更新的操作

其中,每个操作会被表示为一个三元组:[startIndex, endIndex, inc],你需要将子数组 A[startIndex … endIndex](包括 startIndex 和 endIndex)增加 inc。

请你返回 k 次操作后的数组。

示例:

输入: length = 5, updates = [[1,3,2],[2,4,3],[0,2,-2]]
输出: [-2,0,3,5,3]
解释:

初始状态:
[0,0,0,0,0]

进行了操作 [1,3,2] 后的状态:
[0,2,2,2,0]

进行了操作 [2,4,3] 后的状态:
[0,2,5,5,3]

进行了操作 [0,2,-2] 后的状态:
[-2,0,3,5,3]

【思路】

差分数组

如果我们知道每一个元素比前一个元素大多少,以及第一元素的值,我们就可以O(n)遍历数组求出所有数的值。例如:已知a1 = 3, a2比a1大5,则我们就可以知道a2 = a1 + 5 = 8

而本题:每个操作起始代表了这两件事情

  1. a_start比之前一个元素多了inc
  2. a_end+1比前一个元素少了inc

则我们可以建立一个差分数组b表示元素之间的差值,即bi = ai - ai-1,则刚刚的操作就等价于

  1. b_start += inc
  2. b_end+1 -= inc

最后用b数组求出q数组

class Solution {
public:
    vector<int> getModifiedArray(int length, vector<vector<int>>& updates) {
        int len = length;
        vector<int> f(len, 0);
        //第一步:创建差分数组
        for(auto u : updates)
        {
            int start = u[0], end = u[1], diff = u[2];
            //从start下标开始后面的元素都比start前面的元素多diff
            f[start] += diff;
			//直到end+1要把多出的diff减去
            if(end + 1 < len)
                f[end+1] -= diff;
        }
		
        for(int i = 1; i < len; i++)
        {
            f[i] += f[i-1];
        }
        return f;
    }
};

leetcode1109:航班预定统计(就是区间加法)

这里有 n 个航班,它们分别从 1 到 n 进行编号。

有一份航班预订表 bookings ,表中第 i 条预订记录 bookings[i] = [firsti, lasti, seatsi] 意味着在从 firsti 到 lasti (包含 firsti 和 lasti )的 每个航班 上预订了 seatsi 个座位。

请你返回一个长度为 n 的数组 answer,其中 answer[i] 是航班 i 上预订的座位总数。

示例 1:

输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5
输出:[10,55,45,25,25]
解释:
航班编号 1 2 3 4 5
预订记录 1 : 10 10
预订记录 2 : 20 20
预订记录 3 : 25 25 25 25
总座位数: 10 55 45 25 25
因此,answer = [10,55,45,25,25]
示例 2:

输入:bookings = [[1,2,10],[2,2,15]], n = 2
输出:[10,25]
解释:
航班编号 1 2
预订记录 1 : 10 10
预订记录 2 : 15
总座位数: 10 25
因此,answer = [10,25]

提示:

1 <= n <= 2 * 10^4
1 <= bookings.length <= 2 * 10^4
bookings[i].length == 3
1 <= firsti <= lasti <= n
1 <= seatsi <= 10^4

class Solution {
public:
    vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
        //典型区间加法问题,直接套用差分数组模板
        vector<int> res(n);

        for(int i = 0; i < bookings.size(); i++)
        {
            int start = bookings[i][0]-1, end = bookings[i][1]-1, diff = bookings[i][2];
            res[start] += diff;
            if(end+1 < n)
                res[end+1] -= diff;  
        }
        
        for(int i = 1; i < n; i++)
        {
            res[i] += res[i-1];
        }
        return res;
    }
};

leetcode1094:拼车

假设你是一位顺风车司机,车上最初有 capacity 个空座位可以用来载客。由于道路的限制,车 只能 向一个方向行驶(也就是说,不允许掉头或改变方向,你可以将其想象为一个向量)。

这儿有一份乘客行程计划表 trips[][],其中 trips[i] = [num_passengers, start_location, end_location] 包含了第 i 组乘客的行程信息:

必须接送的乘客数量;
乘客的上车地点;
以及乘客的下车地点。
这些给出的地点位置是从你的 初始 出发位置向前行驶到这些地点所需的距离(它们一定在你的行驶方向上)。

请你根据给出的行程计划表和车子的座位数,来判断你的车是否可以顺利完成接送所有乘客的任务(当且仅当你可以在所有给定的行程中接送所有乘客时,返回 true,否则请返回 false)。

示例 1:

输入:trips = [[2,1,5],[3,3,7]], capacity = 4
输出:false
示例 2:

输入:trips = [[2,1,5],[3,3,7]], capacity = 5
输出:true
示例 3:

输入:trips = [[2,1,5],[3,5,7]], capacity = 3
输出:true
示例 4:

输入:trips = [[3,2,7],[3,7,9],[8,3,9]], capacity = 11
输出:true

提示:

你可以假设乘客会自觉遵守 “先下后上” 的良好素质
trips.length <= 1000
trips[i].length == 3
1 <= trips[i] [0] <= 100
0 <= trips[i] [1] < trips[i] [2] <= 1000
1 <= capacity <= 100000

【差分思路】

class Solution {
public:
    bool carPooling(vector<vector<int>>& trips, int capacity) {
        int len = trips.size();
        int n = 0;
        //首先要找到最大的下车地点,目的就是创建res数组
        for(int i = 0; i < len; i++)
        {
            n = max(n, trips[i][2]);
        }

        vector<int> res(n);

        //差分数组经典模板
        for(int i = 0; i < len; i++ )
        {
            int start = trips[i][1] - 1, end = trips[i][2] - 1, diff= trips[i][0];

            res[start] += diff;

            if(end+1 < n)
            {
                res[end] -= diff;
            }
        }
        //区间累加
        for(int i = 1; i < n; i++)
        {
            res[i] += res[i-1];
            //如果超员怎么返回false
            if(res[i] > capacity)
                return false;
        }
        return true;
    }
};

【模拟行为】

leetcode54:螺旋矩阵

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

img

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

提示:

m == matrix.length
n == matrix[i].length
1 <= m, n <= 10
-100 <= matrix[i][j] <= 100

class Solution {
public:
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        vector<int> res;
        int row = matrix.size() -1 ;
        if(row < 0)
            return res;
        int col = matrix[0].size() -1 ;
        if(col < 0)
            return res;
        int up = 0, down = row, left = 0, right = col;
        while(true)
        {
            for(int i = left; i <= right; i++)
            {
                res.push_back(matrix[up][i]);
            }
            up++;
            if(up > down)   break;
            for(int i = up; i <= down; i++)
            {
                res.push_back(matrix[i][right]);
            }
            right--;
            if(right < left)    break;
            for(int i = right; i >= left; i--)
            {
                res.push_back(matrix[down][i]);
            }
            down--;
            if(down < up)   break;
            for(int i = down; i >= up; i--)
            {
                res.push_back(matrix[i][left]);
            }
            left++;
            if(left > right)    break;
        }
        return res;
    }
};

leetcode59:螺旋矩阵II

给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。

img

示例 1:

输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]
示例 2:

输入:n = 1
输出:[[1]]

提示:

1 <= n <= 20

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> matrix(n, vector<int>(n, -1));
        int up = 0, down = n-1, left = 0, right = n-1;
        int num = 1;
        while(true)
        {
            for(int i = left; i <= right; i++)
            {
                matrix[up][i] = num++;
            }
            up++;
            if(up > down) break;
            for(int i = up; i <= down; i++)
            {
                matrix[i][right] = num++;
            }
            right--;
            if(left > right) break;
            for(int i = right; i >= left; i--)
            {
                matrix[down][i] = num++;
            }
            down--;
            if(down < up) break;
            for(int i = down; i >= up; i--)
            {
                matrix[i][left] = num++;
            }
            left++;
            if(left > right) break;
        }
        return matrix;
    }
};

排序

排序算法专题

稳定性:

如果在数组中有两个元素是相等的,在经过某个排序算法之后,原来在前面的的那个元素仍然在另一个元素的前面,那么我们就说这个排序算法是稳定的。

数据结构与算法(一)_第17张图片

如果在排序之后,原来的两个相等元素中在前面的一个元素被移到了后面,那么这个算法就是不稳定的

数据结构与算法(一)_第18张图片

比如排序之前数组为[3(a),2,3(b)](其中a和b分别代表两个不同的3),经过某个排序算法之后是[2,3(a),3(b)],那么这个算法就是稳定的;如果变成了[2,3(b),3(a)],那么这个算法是不稳定的。

再比如在按照身高排队去食堂打饭的过程中,小明和小刚的身高都是170,原来小明在小刚前面,但是经过排序之后小明发现小刚到了他前面了,这样小明肯定对这个不稳定的排序有意见。

比较排序与非比较排序:

如果一个算法需要在排序的过程中使用比较操作来判断两个元素的大小关系,那么这个排序算法就是比较排序,大部分排序算法都是比较排序,比如冒泡排序、插入排序、堆排序等等,这种排序算法的平均时间复杂度最快也只能是O(nlogn)。

非比较排序比较典型的有计数排序、桶排序和基数排序,这类排序能够脱离比较排序时间复杂度的束缚,达到O(n)级别的效率。

算法

void swap(vector<int>& nums, int i, int j){
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

1.冒泡排序

思想:

从左到右依次比较相邻的两个元素,如果前一个元素比较大,就把前一个元素和后一个交换位置,遍历数组之后保证最后一个元素相对于前面的永远是最大的。然后让最后一个保持不变,重新遍历前n-1个元素,保证第n-1个元素在前n-1个元素里面是最大的。依此规律直到第2个元素是前2个元素里面最大的,排序就结束了。

因为这个排序的过程很像冒泡泡,找到最大的元素不停的移动到最后端,所以这个排序算法就叫冒泡排序。

数据结构与算法(一)_第19张图片

//时间复杂度:O(n^2)
//空间复杂度:O(1)
void bubbleSort(vector<int>& nums){
	for(int i = nums.size() - 1; i >= 1; i--){
        for(int j = 1; j <= i; j++){
            if(nums[j-1] > nums[j])
                swap(nums, j , j-1);
        }
    }
}

法是稳定的,在冒泡的过程中如果两个元素相等,他们的位置不会交换

2.选择排序

思想:

先找到前n个元素中最大的值,然后和最后一个元素交换,这样保证最后一个元素一定是最大的然后找到前n-1个元素中的最大值,和第n-1个元素进行交换,然后找到前n-2个元素中最大值,和第n-2个元素交换,依次类推到第2个元素,这样就得到了最后的排序数组。

其实整个过程和冒泡排序差不多,都是要找到最大的元素放到最后,不同点是冒泡排序是不停的交换元素,而选择排序只需要在每一轮交换一次

数据结构与算法(一)_第20张图片

//时间复杂度:O(n^2)
//空间复杂度:O(1)
template<typename T>
//选择排序
/*
对于两层循环每一层都必须完全执行完成,所以任何场景下选择排序都是非常慢的
*/
void selectionSort(T arr[], int n)
{
	for (int i = 0; i < n; i++)
	{
		//寻找[i,n]区间里的最小值
		int minIndex = i;
		for (int j = i + 1; j < n; j++)
		{
			if (arr[j] < arr[minIndex])
				minIndex = j;
		}
		swap(arr[i], arr[minIndex]);
	}
}

选择排序最简单的版本是不稳定的,比如数组[1,3,2,2],表示为[1,3,2(a),2(b)],在经过一轮遍历之后变成了[1,2(b),2(a),3],两个2之间的顺序因为第一个2和3的调换而颠倒了,所以不是稳定排序。

不过可以改进一下选择排序变成稳定的。原来不稳定是因为交换位置导致的,现在如果改成插入操作(不是使用数组而是链表,把最大的元素插入到最后)的话,就能变成稳定排序。比如[1,3,2(a),2(b)],在第一轮中变成了[1,2(a),2(b),3],这样就能够保持相对位置,变成稳定排序。

3.插入排序

插入排序的核心思想是遍历整个数组,保持当前元素左侧始终是排序后的数组,**然后将当前元素插入到前面排序完成的数组的对应的位置,使其保持排序状态。**有点动态规划的感觉,类似于先把前i-1个元素排序完成,再插入第i个元素,构成i个元素的有序数组。

数据结构与算法(一)_第21张图片

//时间复杂度:O(n^2)
//空间复杂度:O(1)
//插入排序【重点】
/*
将为插入的数字放在已插入数字前合适的位置
插入排序对于近乎有序的数组的排序性能甚至比后面的nlogn级别的算法还要快
*/
//template
//void insertionSort(T arr[], int n)
void insertionSort(vector<int>& arr, int n)
{
	//因为第一元素不用考虑
	for (int i = 1; i < n; i++)
	{
		//寻找元素arr[i]合适的插入位置,向前遍历
		for (int j = i; j > 0; j--)//不能等于0
		{
			if (arr[j] < arr[j - 1])
				swap(arr[j], arr[j - 1]);
			else
				break;
		}
	}
}

//template
//插入排序对于近乎有序的数组的排序性能甚至比后面的nlogn级别的算法还要快
//比如对完全有序的数组进行排序将变成一个O(n)级别的算法
//void insertionSort_II(T arr[], int n)
void insertionSort_II(vector<int>& arr, int n)
{
	//因为第一元素不用考虑,因为一个元素根本不用排序
	for (int i = 1; i < n; i++)
	{
		//寻找元素arr[i]合适的插入位置
		int e = arr[i];
		int j;//j保存元素e应该插入的位置
		for (j = i; j > 0; j--)//不能等于0
		{
			if (arr[j - 1] > e)
				arr[j] = arr[j - 1];
			else
				break;
		}
		arr[j] = e;
	}

插入排序是稳定排序,每次交换都是相邻元素的交换,不会有选择排序的那种跳跃式交换元素。

4.希尔排序

解决了插入排序由于经常移动元素而不是用大数据的缺陷

希尔排序在每次的排序的时候都把数组拆分成若干个序列,一个序列的相邻的元素索引相隔的固定的距离gap,每一轮对这些序列进行冒泡或者插入排序,然后再缩小gap得到新的序列一一排序,直到gap为1

比如对于数组[5,2,4,3,1,2],第一轮gap=3拆分成[5,3]、[2,1]和[4,2]三个数组进行插入排序得到[3,1,2,5,2,4];第二轮gap=2,拆分成[3,2,2]和[1,5,4]进行插入排序得到[2,1,2,4,3,5];最后gap=1,全局插入排序得到[1,2,2,3,4,5]

数据结构与算法(一)_第22张图片

//时间复杂度未知
//空间复杂度O(1)
//不稳定排序
void shellSort(vector<int>& nums){
    int gap = nums.size() >> 1;//其实就是nums.size() / 2
    while(gap > 0){
        for(int i = 0; i < gap; i++){
            for(int j = i + gap; j < nums.size(); j = j + gap){
                int temp = j;
                while(temp > i && nums[temp] < nums[temp-gap]){
                    swap(nums, temp, temp - gap);
                    temp = temp - gap;
                }
            }
        }
        gap >>= 1;
    }    
}

【二分插入排序】

class Solution{
public:
    vector<int> sortArray(vector<int>& nums) {
        int n=nums.size();
        for(int i=1;i<n;i++)
        {
            int cur=nums[i];
            int left=0;
            int right=i-1;
            while(left<=right)
            {
                int mid=(left+right)/2;
                if(cur<nums[mid])
                {
                    right=mid-1;
                }
                else
                {
                    left=mid+1;
                }
            }
            for(int j=i-1;j>=left;--j)
            {
                nums[j+1]=nums[j];
            }  
            nums[left]=cur;
        }
        return nums;
    }
};

5. 归并排序【重要】

归并排序是典型的使用分治思想(divide-and-conquer)解决问题的案例。在排序的过程中,把原来的数组变成左右两个数组,然后对左右两个数组分别进行排序,当左右的子数组排序完毕之后,再合并这两个子数组形成一个新的排序数组。 整个过程递归进行,当只剩下一个元素或者没有元素的时候就直接返回。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pt1jFjCt-1626103289601)(C:\Users\mubao\AppData\Roaming\Typora\typora-user-images\image-20210515213804175.png)]

将整个数组划分成两部分,这两部分分别排好序以后,用O(N)的算法将它们归并到一起,形成新的有序数组,如果可以使用递归的过程完成整个归并

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S27bdeAn-1626103289601)(C:\Users\mubao\AppData\Roaming\Typora\typora-user-images\image-20210515214527275.png)]

//时间复杂度:O(nlogn)
//空间复杂度:O(n)
//递归使用归并排序,对nums[left, right]的范围进行排序
void __merge(vector<int>&nums, int left, int mid, int right)
	{
    	//归并数组的核心就是创建新的数组,注意数组的数量是right-left+1
		vector<int> temp_Nums(right - left + 1);
    	//i是左边数组的起点,j是右边数组的起点
		int i = left, j = mid+1;
		int cur = 0; 
		//情况一:i<=mid并且j<=ritght,如果左右都有数据,则通过比较大小来填入数组
		while(i <= mid && j <= right)//开始合并数组
		{
            //如果nums[j]大于等于nums[i],则把nums[i]添加到数组中,i++;否则将nums[j]添加到数组中,j++
			if(nums[i] <= nums[j])
				temp_Nums[cur] = nums[i++];
			else
				temp_Nums[cur] = nums[j++];
			cur++;
		}
		//可能一个数组已经用完,则把剩下的数组全部放入temp中
		while(i <= mid)//情况二:i <= mid 并且 j > right
			temp_Nums[cur++] = nums[i++];
		while(j <= right)//情况三:i > mid 并且 j <= right
			temp_Nums[cur++] = nums[j++];
    //将排好序的数组赋值给原来数组
		for(int k = 0; k < temp_Nums.size(); k++)//合并数组完成,拷贝到原来的数组
		{
			nums[left + k] = temp_Nums[k];
		}
	}
	void __mergeSort(vector<int>& nums, int left, int right)
	{
        //因为需要递归,所以必须要有终止条件
		if(left >= right)//等于都不行吗??,因为如果left == right就没有办法分成两个数组尽行归并了
			return;
		
		int mid = left + (right - left) / 2;
        //[left, mid]进行归并排序
		__mergeSort(nums, left, mid);//
        //[mid + 1, right]进行归并排序
		__mergeSort(nums, mid + 1, right);
        //合并数组
		__merge(nums, left, mid, right);
	}	
	//输入参数:nums:要排序数组;n:该数组的长度
	void mergeSort(vector<int>& nums, int n)
	{
		__mergeSort(nums, 0, n-1);
	}

归并排序是稳定排序,保证原来相同的元素能够保持相对的位置。

归并排序的优化:

上一个程序无论nums是什么都要进行merge操作(合并)
其实如果nums[mid] <= nums[mid+1]就不用进行+1操作
void __mergeSort(vector<int>& nums, int left, int right)
	{
		if(left >= right)
			return;
		/*
		if(right - left <= 15)
			insertionSort(nums, left, right);
			return;
		
		*/
		int mid = left + (right - left) / 2;
        //[left, mid]进行归并排序
		__mergeSort(nums, left, mid);
        //[mid+1, right]进行归并排序
		__mergeSort(nums, mid + 1, right);
        //合并数组
        if(nums[mid] > nums[mid+1])//如果左侧的最大比右侧的最小的数字大才需要进行归并
			__merge(nums, left, mid, right);
	}

归排序背诵模板

void merge_sort(vector<int> & nums, int l, int r)
{
     if(l >= r)
         return;
    int mid = (l+r)/2;
    merge_sort(nums,l, mid);
    merge_sort(nums,mid+1, r);
    int k = 0, i = l, j = mid+1;
    vector<int> temp = nums;
    while(i <= mid && j <= r)
    {
        if(nums[i] < nums[j])
        {
            temp[k++] = nums[i++];
        }
        else
        {
            temp[k++] = nums[j++];
        }
    }
    if(i <= mid)
    {
        temp[k++] = nums[i++];
    }
    else
    {
        temp[k++] = nums[j++];
    }
    nums = temp;
}

6.快速排序【重要】

其核心的思路是取第一个元素(或者最后一个元素)作为分界点,把整个数组分成左右两侧,左边的元素小于或者等于分界点元素,而右边的元素大于分界点元素,然后把分界点移到中间位置,对左右子数组分别进行递归,最后就能得到一个排序完成的数组。当子数组只有一个或者没有元素的时候就结束这个递归过程。

其中最重要的是将整个数组根据分界点元素划分成左右两侧的逻辑,目前有两种算法,图片展示的是第一种。

数据结构与算法(一)_第23张图片

    //对nums[l,r]部分进行partition操作
//返回p,使得nums[l...p-1]arr[p]
int __partition(vector<int>& nums, int l, int r)
{
	int v = nums[l];
	//arr[l+1....j]v
	//通过初始定义来让初始数组为空
	int j = l;//j用于记录小于v与大于v的分界点
	for (int i = l + 1; i <= r; i++)
	{
        // v是最左侧数组的值
		if (nums[i] < v)//如果nums[i]>v什么都不用做
		{
			swap(nums[j + 1], nums[i]);
			j++;
		}
	}
	swap(nums[l], nums[j]);
	return j;
}
//对nums[l...r]部分进行快速排序,注意左右边界
void __quickSort(vector<int>& nums, int l, int r)
{
    //与归并排序思想差不多
	if (l >= r)
		return;
	int p = __partition(nums, l, r);//经过这一步操作p位置就已经确定数据了,不需要在动p位置了,所有后面是[l,p-1]快排,[p+1,r]快排
	__quickSort(nums, l, p - 1);
	__quickSort(nums, p + 1, r);
}
//快速排序
/*
输入参数:
	nums:想要排序的数组;
	   n:nums的长度大小
*/
void quickSort(vector<int>& nums, int n)
{
	__quickSort(nums, 0, n - 1);
}

对于近乎有序的数组,归并排序与快速排序的性能对比,快速排序比归并排序慢了太多。对于完全有序的数组的快速排序最差情况退化成O(n^2),随机化快排序法

    //对nums[l,r]部分进行partition操作
//返回p,使得nums[l...p-1]arr[p]
int __partition(vector<int>& nums, int l, int r)
{
    //生成一个随机索引与l位置数据交换
    swap(nums[l], nums[rand()%(r-l+1)+l]);//生成一个[l,r]一个随机索引
	int v = nums[l];
	//arr[l+1....j]v
	//通过初始定义来让初始数组为空
	int j = l;
	for (int i = l + 1; i <= r; i++)
	{
		if (nums[i] < v)//如果nums[i]>v什么都不用做
		{
			swap(nums[j + 1], nums[i]);
			j++;
		}
	}
	swap(nums[l], nums[j]);
	return j;
}
//对nums[l...r]部分进行快速排序
void __quickSort(vector<int>& nums, int l, int r)
{
	if (l >= r)
		return;
	int p = __partition(nums, l, r);
	__quickSort(nums, l, p - 1);
	__quickSort(nums, p + 1, r);
}
//快速排序
void quickSort(vector<int>& nums, int n)
{
    srand(time(NULL));//随机种子
	__quickSort(nums, 0, n - 1);
}

快速排序是不稳定排序

双路快速排序法—》如果有大量重复键值的情况下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kGdnDop1-1626103289603)(C:\Users\mubao\AppData\Roaming\Typora\typora-user-images\image-20210517111110783.png)]

//对nums[l,r]部分进行partition操作
//返回p,使得nums[l...p-1]arr[p]
int __partition2(vector<int>& nums, int l, int r)
{
    swap(nums[l], nums[rand()%(r-l+1)+l]);//生成一个[l,r]一个随机索引
	int v = nums[l];
	//arr[l+1....i)<=v;arr(j,...,r]>=v
	//通过初始定义来让初始数组为空
    int i = l+1, j=r;//为什么i=l+1是因为nums[l]已经作为标杆了
    while(true)
    {
		while(i <= r && nums[i] < v) i++;
        while(j >= l+1 && nums[j] > v) j--;
        if(i > j)break;
        swap(nums[i], nums[j]);
        i++;
        j--;
    }
    swap(nums[l], nums[j]);//因为下标j对应的才是小于v的数字
	return j;
}
//对nums[l...r]部分进行快速排序
void __quickSort2(vector<int>& nums, int l, int r)
{
	if (l >= r)
		return;
    /*
    if(r-l<=15)
    {
    	insertionSort(nums, l, r);
    	return;
    }
    */
	int p = __partition2(nums, l, r);
	__quickSort2(nums, l, p - 1);
	__quickSort2(nums, p + 1, r);
}
//快速排序
void quickSort2(vector<int>& nums, int n)
{
    srand(time(NULL));//随机种子
	__quickSort2(nums, 0, n - 1);
}

三路快排------》不需要对等于v的大量元素进行操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5adG8tW5-1626103289604)(C:\Users\mubao\AppData\Roaming\Typora\typora-user-images\image-20210517112527715.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ly7dqGMn-1626103289605)(C:\Users\mubao\AppData\Roaming\Typora\typora-user-images\image-20210517112716835.png)]

//对nums[l,r]部分进行partition操作
//返回p,使得nums[l...p-1]arr[p]
int __partition2(vector<int>& nums, int l, int r)
{
    swap(nums[l], nums[rand()%(r-l+1)+l]);//生成一个[l,r]一个随机索引
	int v = nums[l];
	//arr[l+1....i)<=v;arr(j,...,r]>=v
	//通过初始定义来让初始数组为空
    int i = l+1, j=r;
    while(true)
    {
		while(i <= r && nums[i] < v) i++;
        while(j >= l+1 && nums[j] > v) j--;
        if(i > j)break;
        swap(nums[i], nums[j]);
        i++;
        j--;
    }
    swap(nums[l], nums[j]);
	return j;
}
//对nums[l...r]部分进行快速排序
void __quickSort3Ways(vector<int>& nums, int l, int r)
{
	if (l >= r)
		return;
    /*
    if(r-l<=15)
    {
    	insertionSort(nums, l, r);
    	return;
    }
    */
    //partition
    swap(nums[l],nums[rand() % (r-l+1)+l]);
    int v = nums[l];
    int lt = l;//arr[l+1...lt]
    int gt = r+1;//arr[gt...r]>v
    int i = l+1;//arr[lt+1...i]==v
    while(i < gt)
    {
        if(nums[i]<v)
        {
            swap(nums[i], nums[lt+1]);
            lt++;
            i++;
        }
        else if(nums[i] > v)
        {
            swap(nums[i], nums[gt-1]);
            gt--;
            i++;
        }
        else//arr[i] == v
        {
            i++;
        }
    }
    swap(nums[l],nums[lt]);
	__quickSort3Ways(nums, l, lt - 1);
	__quickSort3Ways(nums, gt, r);
}
//快速排序
void quickSort3Ways(vector<int>& nums, int n)
{
    srand(time(NULL));//随机种子
	__quickSort3Ways(nums, 0, n - 1);
}

快排背诵模板

上面的代码都是有助于你优化和学习快排,要熟悉每一种快排的应用场景,但是在笔试中,快排直接默写模板。

    void quick_sort(vector<int>& nums, int l, int r)
    {
        if(l >= r)
            return;
        int x = nums[(r+l)>>1], i = l - 1, j = r + 1;
        while(j > i)
        {
            while(nums[++i] < x);
            while(nums[--j] > x);
            if(i < j)
                swap(nums[i], nums[j]);
        }
        quick_sort(nums,  l,  j);
        quick_sort(nums, j + 1, r);
    }

7.堆排序

为什么使用优先队列?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pQtPTy7f-1626103289606)(C:\Users\mubao\AppData\Roaming\Typora\typora-user-images\image-20210517172306423.png)]

强调动态性,优先队列的场景适用在动态的情况

比如服务器对请求的回应就是优先队列;

在100000000个元素中选出前100小元素,如果将N个元素都排序的话是NlogN的时间复杂度。如果使用一个最小堆 保持最小堆这里的元素一直是小于等于100的,不断的插入新元素,最终最小堆中存放的元素就是前100小的元素

C++优先级队列的用法

堆的细节优化

ShiftUp和ShiftDown中使用复制操作替换swap操作

表示堆的数组从0开始索引

没有capacity的限制,动态的调整堆中数组的大小

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZpEXSms6-1626103289606)(C:\Users\mubao\AppData\Roaming\Typora\typora-user-images\image-20210517190700530.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tjozLpID-1626103289607)(C:\Users\mubao\AppData\Roaming\Typora\typora-user-images\image-20210517191050362.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2wuAuQj2-1626103289607)(C:\Users\mubao\AppData\Roaming\Typora\typora-user-images\image-20210517191320984.png)]

注意下标是从1开始取,同时黑色的是索引,红圈里面的是数据值

#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

template<typename Item>

class MaxHeap{
	Item* data;
	int count;
	int capacity;
	//将k索引的元素向上移动目的是维持堆的定义
	void shiftUp(int k)
	{
		while(k > 1 && data[k/2] < data[k])//k位置的父节点的值是否比k位置的值小
		{
			swap(data[k/2], data[k]);//只要有数组就要防止数组越界
			k /= 2;
		}
	}
	//传入一个k尝试将k位置的索引向下移动
	void shiftDown(int k)
	{
		while(2*k <= count)
		{
			int j = 2 * k;//在此轮循环中,data[k]和data[j]交换位置
		if(j+1 <= count && data[j+1] > data[j])//data[j+1]右孩子是否比data[j]左孩子还要大
			j += 1;//data[k]与k的右孩子互换
		if(data[k] >= data[j])//父节点如果大于等于它的两个孩子,不需要交换
			break;
		swap(data[k], data[j]);
		k = j;
		}
	}
public:
	MaxHeap(int capacity)
	{
		data = new Item[capacity + 1];
		count = 0;
		this->capacity = capacity;
	}
    
    //将数组整合成堆的样子
    MaxHeap(Item arr[], int n )
    {
        data = new Item[n+1];
        capacity = n;
        for(int i = 0; i < n; i++)
        {
            data[i+1] = arr[i];
        }
        count = n;
        //i的父亲节点是count/2
        for(int i = count / 2; i >= 1; i--)
        {
            shiftDown(i);
        }
    }
	~MaxHeap()
	{
		delete [] data;
	}
	int size()
	{
		return count;
	}
	bool isEmpty()
	{
		return count == 0;
	}
	//Shift Up
	void insert(Item item)
	{
		assert(count + 1 <= capacity);
		data[count + 1] = item;
		count++;
		shiftUp(count);
	}
	//shift Down
	Item extractMax()
	{
		assert(count>0);
		Item ret = data[1];
		swap(data[1], data[count]);
		count--;
		shiftDown(1);
		return ret;
	}
	
}

int main()
{
	MaxHeap<int> maxheap = MaxHeap<int>(100);
	srand(time(NULL));
	for(int i = 0; i < 15;i++)
		maxheap.insert(rand()%100);//插入的元素是[0,100)前闭后开的区间的随机数
	return 0;
//使用堆排序
void heapSort(vector<int>& nums, int n)
{
	MaxHeap<int> maxheap = MaxHeap<int>(n);
	for(int i = 0; i < n; i++)
		maxheap.insert(nums[i]);
	for(int i = n-1; i >= 0; i--)
	{
		nums[i] = maxheap.extractMax();
	}
}
void heapSort2(vector<int>& nums, int n)
{
    MaxHeap<int> maxheap = maxHeap<int>(nums,n);
    for(int i = n-1; i>=0; i--)
    {
        for(int i = n-1; i >= 0 ;i--)
        {
            nums[i] = maxheap.extractMax();
        }
    }
}

第二个版本的堆排序比第一版的略快。

将n个元素逐个插入到空堆中,算法时间复杂度是nlogn

而heapify的过程,算法的时间复杂度是O(n)

**稳定性:**对于相等的元素,在排序后,原来靠前的元素依然靠前,相等元素的相对位置没有发生改变。

可以通过自定义比较函数,让排序算法不存在稳定性的问题。

堆排序模板

#include 
using namespace std;
const int N = 1e5+10;
int h[N];
int n , m, Size;

void down(int u)
{
    int t = u;
    //如果存在左节点并且左节点小于当前节点,因为下标是从1开始的所以左右节点的下标是可能等于size的
    if(2 * u <= Size && h[2 * u] < h[t]) t = 2 * u;
    //如果存在右节点并且右节点小于当前节点
    if(2 * u + 1 <= Size && h[2 * u + 1] < h[t]) t = 2 * u + 1;
    //如果t不等于u就说明子节点有小于当前节点的数,所以要进行递归
    if(t != u)
    {
        swap(h[t], h[u]);
        down(t);
    }
}
int main()
{
    cin >> n >> m;
    //注意因为堆是平衡二叉树用数组模拟,为了解决根节点为0的情况,我们从1开始插入数组
    for(int i = 1; i <= n; i++ ) cin >> h[i];
    Size = n;
    /*
    为什么可以这样初始化,可能是取出最小之后都是最后一位数插入到最前面在进行down操作*/
    for(int i = n / 2; i; i--) down(i);
    while(m--)
    {
        cout<<h[1]<<" ";
        h[1] = h[Size];
        Size--;
        down(1);
    }
    return 0;
}
bool operator<(const Student& otherStudent)
{
	return score != otherStudent.sore?
				score > otherStudent.score:
				name < otherStudent.name;
}

索引堆

以最大索引堆为例。
构建最大堆之后,只是将索引值改变了:元素比较的时候比较的是data数据,但是交换的时候交换的index索引。

class MaxHeap{
	Item* data;
    int* indexes;
	int count;
	int capacity;
	//将k索引的元素向上移动目的是维持堆的定义
	void shiftUp(int k)//索引数组的
	{
		while(k > 1 && data[indexes[k/2]] < data[indexes[k])//k位置的父节点的值是否比k位置的值小
		{
			swap(indexes[k/2], indexes[k]);//只改变索引
			k /= 2;
		}
	}
	//传入一个k尝试将k位置的索引向下移动
	void shiftDown(int k)
	{
		while(2*k <= count)
		{
			int j = 2 * k;//在此轮循环中,data[k]和data[j]交换位置
		if(j+1 <= count && data[indexes[j+1]] > data[indexes[j]])//data[j+1]右孩子是否比data[j]左孩子还要大
			j += 1;//data[k]与k的右孩子互换
		if(data[indexes[k]] >= data[indexes[j]])//父节点如果大于等于它的两个孩子,不需要交换
			break;
		swap(indexes[k], indexes[j]); 
		k = j;
		}
	}
public:
	IndexMaxHeap(int capacity)
	{
		data = new Item[capacity + 1];
        indexes = new Item[capacity+1]
		count = 0;
		this->capacity = capacity;
	}
    
   	~MaxHeap()
	{
		delete [] data;
        delete [] indexes;
	}
	int size()
	{
		return count;
	}
	bool isEmpty()
	{
		return count == 0;
	}
	//Shift Up
    //传入的i对用户而言是从0开始的
	void insert(int i, Item item)
	{
		assert(count + 1 <= capacity);
        assert(i+1 >= 1 && i + 1 <= capacity);
        i += 1;
        data[i] = item;
        indexes[count+1] = i;
		count++;
		shiftUp(count);
	}
	//shift Down
	Item extractMax()
	{
		assert(count>0);
		Item ret = data[indexes[1]];
		swap(indexes[1], indexes[count]);
		count--;
		shiftDown(1);
		return ret;
	}
	Item extractMaxIndex()
	{
		assert(count>0);
		Item ret = indexes[1]-1;//索引是从1开始转成从0开始
		swap(indexes[1], indexes[count]);
		count--;
		shiftDown(1);
		return ret;
	}
    Item getItem(int i)                                             {
        return data[i+1];//输入的i是从0开始的索引,我们将i+1转成从1开始的索引
    }
    void change(int i, Item newItem)
    {//时间复杂度:O(n)遍历 + O(logn)shiftup或down
        i += 1;
        data[i] = newItem;
        //找到indexes[j] = i,j表示data[i]
        //之后是shiftUp(j),再shiftDown(j)
        for(int j = 1; j <= count; j++)
        {
            if(indexes[j] == i)
            {
                shiftUp(j);
                shiftDown(j);
                return;
            }
        }
    }                                                 
}

索引堆的优化:使用反向查找数组reverse

无论是索引和数据分离开还是加上反向查找表都是我们应该会的思想与方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M2OIclF8-1626103289610)(C:\Users\mubao\AppData\Roaming\Typora\typora-user-images\image-20210518153736858.png)]

class MaxHeap{
	Item* data;
    int* indexes;
    int* reverse;
	int count;
	int capacity;
	//将k索引的元素向上移动目的是维持堆的定义
	void shiftUp(int k)//索引数组的
	{
		while(k > 1 && data[indexes[k/2]] < data[indexes[k])//k位置的父节点的值是否比k位置的值小
		{
			swap(indexes[k/2], indexes[k]);//只改变索引
			reverse[indexes[k/2]] = k / 2;
            reverse[indexes[k]] = k;
            k /= 2;
		}
	}
	//传入一个k尝试将k位置的索引向下移动
	void shiftDown(int k)
	{
		while(2*k <= count)
		{
			int j = 2 * k;//在此轮循环中,data[k]和data[j]交换位置
		if(j+1 <= count && data[indexes[j+1]] > data[indexes[j]])//data[j+1]右孩子是否比data[j]左孩子还要大
			j += 1;//data[k]与k的右孩子互换
		if(data[indexes[k]] >= data[indexes[j]])//父节点如果大于等于它的两个孩子,不需要交换
			break;
		swap(indexes[k], indexes[j]); 
        reverse[indexes[k]] = k;
        reverse[index[j]] = j;
 		k = j;
		}
	}
public:
	IndexMaxHeap(int capacity)
	{
		data = new Item[capacity + 1];
        indexes = new Item[capacity+1];
		reverse = new Item[capacity+1];
        for(int i = 0; i <= capacity; i++)
        {
            reverse[i] = 0;
        }
        count = 0;
		this->capacity = capacity;
	}
    
   	~MaxHeap()
	{
		delete [] data;
        delete [] indexes;
        delete [] reverse;
	}
	int size()
	{
		return count;
	}
	bool isEmpty()
	{
		return count == 0;
	}
	//Shift Up
    //传入的i对用户而言是从0开始的
	void insert(int i, Item item)
	{
		assert(count + 1 <= capacity);
        assert(i+1 >= 1 && i + 1 <= capacity);
        i += 1;
        data[i] = item;
        indexes[count+1] = i;
        reverse[i] = count + 1;
		count++;
		shiftUp(count);
	}
	//shift Down
	Item extractMax()
	{
		assert(count>0);
		Item ret = data[indexes[1]];
		swap(indexes[1], indexes[count]);
        reverse[indexes[1]] = 1;
        reverse[indexes[count]] = 0;
        count--;
		shiftDown(1);
		return ret;
	}
	Item extractMaxIndex()
	{
		assert(count>0);
		Item ret = indexes[1]-1;//索引是从1开始转成从0开始
		swap(indexes[1], indexes[count]);
        reverse[indexes[1]] = 1;
        reverse[indexes[count]] = 0;
		count--;
		shiftDown(1);
		return ret;
	}
    bool contain(int i)
  	{
        assert(i+1>=1 && i+1 <= capacity)
        //判断堆中是否包含索引i
        return reverse[i+1] != 0;
                     	                                
  	}
    Item getItem(int i)
    {
        assert(contain(i));
        return data[i+1];//输入的i是从0开始的索引,我们将i+1转成从1开始的索引
    }
    void change(int i, Item newItem)//保证当前索引堆包含索引
    {//时间复杂度:O(n)遍历 + O(logn)shiftup或down
        assert(contain(i));//防止数组越界
        i += 1;
        data[i] = newItem;
        //找到indexes[j] = i,j表示data[i]
        //之后是shiftUp(j),再shiftDown(j)
       int j = reverse[i];
        shiftUp(j);
        shiftDown(j);
    }                                                 
}

选择排序的优化 不稳定O(nlogn)

堆排序是一个效率要高得多的选择排序,首先把整个数组变成一个最大堆,**然后每次从堆顶取出最大的元素,这样依次取出的最大元素就形成了一个排序的数组。**堆排序的核心分成两个部分,第一个是新建一个堆,第二个是弹出堆顶元素后重建堆。

新建堆不需要额外的空间,而是使用原来的数组,一个数组在另一个维度上可以当作一个完全二叉树(除了最后一层之外其他的每一层都被完全填充,并且所有的节点都向左对齐),对于下标为i的元素,他的子节点是2i+1和2i+2(前提是没有超出边界)。在新建堆的时候从左向右开始遍历,当遍历到一个元素的时候,重新排列从这个元素节点到根节点的所有元素,保证满足最大堆的要求(父节点比子节点要大)。遍历完整个数组的时候,这个最大堆就完成了。

在弹出根节点之后(把根节点的元素和树的最底层最右侧的元素互换),堆被破坏,需要重建。从根节点开始和两个子节点比较,如果父节点比最大的子节点小,那么就互换父节点和最大的子节点,然后把互换后在子节点位置的父节点当作新的父节点,和它的子节点比较,如此往复直到最后一层,这样最大堆就重建完毕了。

数据结构与算法(一)_第24张图片

8.计数排序

记录min&max,通过创建跨度这么大的数组用于计数,然后进行还原。稳定

class Solution{
public:
    vector<int> sortArray(vector<int>& nums) {
        int n=nums.size();
        if(n<=1)return nums;
        int min=INT_MAX,max=INT_MIN;
        for(int i=0;i<n;++i)
        {
            if(nums[i]>max)max=nums[i];
            if(nums[i]<min)min=nums[i];
        }
        int k=max-min+1;//计数数组长度
        vector<int> count(k,0);
        for(int i=0;i<n;++i)
        {
            count[nums[i]-min]+=1;
        }
        int loc=0;
        for(int i=0;i<k;++i)
        {
            while(count[i]>0)
            {
                nums[loc++]=i+min;
                count[i]-=1;
            }
        }
        return nums;
    }
};

9.桶排序

桶排序,就是分区间,然后元素对应放在每个区间中再排序(插入排序,因为这里假设每个桶的元素都很少),最后顺序输出。

class Solution{
public:
    vector<int> sortArray(vector<int>& nums) {
        int n = nums.size();
        if(n <= 1)return nums;
        int min=INT_MAX,max=INT_MIN;
        for(int i=0;i<n;++i)
        {
            if(nums[i]>max)max=nums[i];
            if(nums[i]<min)min=nums[i];
        }
        int bucketNum=(max-min)/n+1;//确定桶数
        
        vector<vector<int>> bucket(bucketNum);
        for(int i=0;i<n;++i)//数据放入桶
        {
            int num=(nums[i]-min)/n;
            bucket[num].push_back(nums[i]);
        }
        int count=0;
        for(int i=0;i<bucketNum;++i)
        {
            if(!bucket[i].empty())
            {
                sort(bucket[i].begin(),bucket[i].end());
                for(int j=0;j<bucket[i].size();++j)
                {
                    nums[count++]=bucket[i][j];
                }
            }  
        }
        return nums;
    }
};

10.基数排序

以位数开始进行元素排序,时间复杂度为O(n*k)

class Solution{
public:
    vector<int> sortArray(vector<int>& nums) {
        int n=nums.size();
        if(n<=1)return nums;
        int min=INT_MAX,max=INT_MIN;
        for(int i=0;i<n;++i)
        {
            if(nums[i]>max)max=nums[i];
            if(nums[i]<min)min=nums[i];
        }
        max=max>(-min)?max:-min;//考虑到负数影响最大值和最小值的位数,选取绝对值最大的数
        int digit=0;
        while(max>0)
        {
            max /= 10;
            ++digit;
        }
        vector<vector<int>> bucket(19);
        int pos;
        int cur;
        for(int i=0,mod=1;i<digit;++i,mod*=10)
        {
            for(int j=0;j<n;++j)
            {
                pos=(nums[j]/mod)%10;
                bucket[pos+9].push_back(nums[j]);
            }
            cur=0;
            for(int j=0;j<19;++j)
            {
                for(int k=0;k<bucket[j].size();++k)
                {
                    nums[cur++]=bucket[j][k];
                }
                bucket[j].clear();
            }
        }
        return nums;
    }
};

leetcode75:颜色分类

【题目】

给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

 

示例 1:

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]

- `n == nums.length`
- `1 <= n <= 300`
- `nums[i]` 为 `0`、`1` 或 `2

区间内大量重复的数据,可以采用三路快排

【思路】

  1. 计数排序:分别统计0,1,2的元素个数
  2. 三路快排

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sdlwa0sZ-1626103289612)(C:/Users/Admin/AppData/Roaming/Typora/typora-user-images/image-20210216213210209.png)]

class Solution {
public:
    void sortColors(vector<int>& nums) {
        int zeronums = 0;
        int onenums = 0;
        int twonums = 0;
        vector<int> result;
        for(int i = 0; i < nums.size(); i++){
            if(nums[i] == 0)
                zeronums++;
            else if (nums[i] == 1)
                onenums++;
            else if(nums[i] == 2)
                twonums++;
        }
        for(int i = 0; i < zeronums; i++){
            result.push_back(0);
        }
         for(int i = 0; i < onenums; i++){
            result.push_back(1);
        }
         for(int i = 0; i < twonums; i++){
            result.push_back(2);
        }
        nums = result;
    }
};

#include
class Solution_75_1{
public:
	//计数排序:元素个数非常有限的排序场景
	//时间复杂度:O(n)
	//空间复杂度:O(k)也就是O(1);
	void sortColors(vector<int>& nums) {
		int count[3] = { 0 };//存放0,1,2三个元素的频率
		for (int i = 0; i < nums.size(); i++) {
			assert(nums[i] >= 0 && nums[i] <= 2);
			count[nums[i]]++;//只要提到数组就要考虑是否越界的问题
		}
		int index = 0;
		for (int i = 0; i < count[0]; i++)nums[index++] = 0;
		for (int i = 0; i < count[1]; i++)nums[index++] = 1;
		for (int i = 0; i < count[2]; i++)nums[index++] = 2;
	}
};
class Soluton_75_2 {
public:
	//Quick Sort 3Ways
	//时间复杂度:O(n);
	//空间复杂度:O(1);
	void sortColors(vector<int>& nums) {
		int zero = -1;//nums[0...zero]==0
		int two = nums.size();//nums[two...n-1]==2
        //这个条件与正常的循环不同
		for (int i = 0; i < two;) {
			if (nums[i] == 1) i++;
			else if (nums[i] == 2) {
  				//为什么是先减呢是因为nums【two。。。n-1】范围内是不存在,只有先减一才会使[two...n-1]成立[n-1...n-1]有一个元素
				two--;
				swap(nums[i], nums[two]);
			}
			else{//nums[i]==0
				assert(nums[i] == 0);
				zero++;
				swap(nums[zero], nums[i]);//为什么要i++而不是重新判断,是因为就是从i遍历的所以nums[i]肯定是1不可能是2,因此不需要比较
				i++;
			}
		}
	}
};

void main() {
	vector<int> nums = { 0 };
	Solution_75_1().sortColors(nums);
	system("pause");
	return;
}

leetcode88:合并两个有序数组

【题目】

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。

初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。你可以假设 nums1 的空间大小等于 m + n,这样它就有足够的空间保存来自 nums2 的元素。

示例 1:

输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]

示例 2:

输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
//练习题:88 Merge Sorted Array(归并排序)
//以空间换时间
class Solution_88 {
public:
	void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        //【重点】归并排序会引入额外的数组
		vector<int> result;
        //定义两个指针分别遍历两个数组
		int p1 = 0, p2 = 0;
		while (p1 < m || p2 < n) {
            //情况一:nums1向result数组中插入所有数时,后面将nums2插入result
			if (p1 == m) {
				result.push_back(nums2[p2]);
				p2++;
			}
            //情况二:nums2向result数组插入所有数时,后面讲nums1插入result
			else if (p2 == n) {
				result.push_back(nums1[p1]);
				p1++;
			}
            //情况三:nums1与nums2同时有值的时候,讲nums中较小的数值插入result中
			else if (nums1[p1] > nums2[p2]) {
				result.push_back(nums2[p2]);
				p2++;
			}
			else if(nums1[p1] <= nums2[p2]) {
				result.push_back(nums1[p1]);
				p1++;
			}
		}
        //因为函数返回中是空所以需要赋值
		nums1 = result;
	}
};

void main_88() {
	vector<int> nums1 = { 1,2,3,0,0,0 };
	vector<int> nums2 = { 2,5,6 };
	Solution_88().merge(nums1, 3, nums2, 3);
	return;
}

你可能感兴趣的:(数据结构与算法,算法,数据结构)