LeetCode算法——数组/字符串篇

对刷过的算法进行总结,所用解法都是最符合我个人逻辑的,以后再刷的话就看这篇帖子了


# 代码随想录——数组理论基础

首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题

数组是存放在连续内存空间上的相同类型数据的集合

数组可以方便的通过下标索引的方式获取到下标下对应的数据。

举一个字符数组的例子,如图所示:

需要两点注意的是

  • 数组下标都是从0开始的。
  • 数组内存空间的地址是连续的

正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。

大家如果使用C++的话,要注意 vector 和 array 的区别,vector 的底层实现是 array,严格来讲vector 是容器,不是数组。数组的元素是不能删的,只能覆盖。


1、买卖股票的最佳时机

题目描述:

解法:

        只用遍历一次数组,思路分为2部分:

  1. 使用数组中的首元素作为股票最低价格(minPrice),使用for循环遍历数组,找到真正的minPrice;
  2. 卡住数组中临时的一个 minPrice 时,进入 else 来获取当前的利润(profit),这样不断往复,最终会获取到题目要求的最大利润。这样的解法十分优美!
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int profit = 0;
        int minPrice = prices[0];

        for(int i = 1; i < prices.size(); ++i){
            
            // 找到数组中的最小元素
            if(prices[i] < minPrice){
                minPrice = prices[i];
            }
            // 找到最大利润
            else{
                if(prices[i] - minPrice > profit){
                    profit = prices[i] - minPrice;
                }
            }
        }
        return profit;
    }
};

2、移除元素

题目描述:

解法:

        题目要求原地修改原数组,但数组的特性是:数组中的元素只能覆盖,不能删除。所以设一个新的索引 index 来重新排列修改后的数组。

        使用 for 循环遍历数组时,如果 nums[i]  == val, 则continue程序不往下继续进行,而是进入下一次循环。当下一个  nums[i] != val 时,则将当前元素的索引赋值给新的索引 nums[index], 并且 index++ , 准备好接收下一个 != val 的数组元素。

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {

        int index = 0;
        for(int i = 0; i < nums.size(); ++i){
            if(nums[i] == val){

                // 跳过下面的赋值,进入下一次循环
                continue;
            }

            nums[index] = nums[i];
            index++;
        }
        return index;
    }
};

3、合并两个有序数组

题目描述:

解法:

       利用了2个数组非递减的特性,使用尾指针从后向前往 nums1 中加入元素。率先使用 resize 将 nums1 的长度扩大到 m + n,以便向 nums1 中加入新元素。

        设置这样的 while 循环判断条件是考虑到2个数组总会有一方的索引先 < 0, 在消耗完其中一方时,再接一个 while 循环将另一方数组中的剩余元素添加至新的数组中

class Solution {
public:
    void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
        
        // 要将nums2合并到nums1中,nums1数组长度发生改变,定义新的长度以便定义新的尾指针
        nums1.resize(m + n);

        // 定义数组的尾指针
        int i = m - 1;
        int j = n - 1;
        int index = m + n - 1;

        while(i >= 0 && j >= 0){
            if(nums1[i] < nums2[j]){
                nums1[index] = nums2[j];
                index--;
                j--;
            }
            else{
                nums1[index] = nums1[i];
                index--;
                i--;
            }
        }

        while(i >= 0){
            nums1[index] = nums1[i];
            index--;
            i--;
        }

        while(j >= 0){
            nums1[index] = nums2[j];
            index--;
            j--;
        }
    }
};

4、删除有序数组中的重复项

题目描述:

解法:

       为数组首元素设置慢指针 slow,第二个元素设置快指针 fast,指针 fast 向后遍历,直至越界。指针 slow 只在指针 fast 遇到不重复项时向后移动一位,以此接收不重复项。最终返回 slow + 1即新数组的长度

        题目中提到,返回的是最终的数组长度 ,是一个整数。但为什么输出的答案是数组呢? 输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的,根据我们函数返回的长度,它会打印出该长度范围内所有的元素

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {

        int slow = 0;
        int fast = 1;
        
        while(fast < nums.size()){

            if(nums[fast] != nums[slow]){
                
                // 出现不重复的元素,用++slow去接收
                ++slow;
                nums[slow] = nums[fast];
            }
            else{
                // 出现重复元素,slow不动,fast往后走
                fast++;
            }
        }
        return slow + 1;
    }
};

5、删除有序数组中的重复项Ⅱ

题目描述:

解法:

        首先给出第一次 AC 了8%测试用例的版本,方便记录我忽略了这道题[最关键的信息]

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {

        int slow = 1;
        int fast = 3;

        while(fast < nums.size()){
            if(nums[fast] == nums[slow] && nums[fast - 1] == nums[slow]){
                slow++;
                nums[slow] = nums[fast - 1];
                fast++;
            }
            else{
                slow++;
                nums[slow] = nums[fast];
                fast++;
            }
        }

        return slow + 1;
    }
};

        由于这段代码的逻辑完全建立在第一个测试用例上

nums = [1,1,1,2,2,3]

        可以处理前 3 个元素相同时的情况,但如果是下面这种情况:

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

        会将第二个元素 0 覆盖掉,很愁啊,修改了判断条件后连第一个测试用例都 AC 不过了!

        那么关键是什么呢?答:“数组的前两个元素必然满足条件,无需处理”这一点官方解答中也提到了。

        所以慢指针 slow 设在索引为 2 的位置上,使用一个循环,从索引为 2的位置开始遍历数组。对于当前遍历的元素,如果它与慢指针 slow 前两个位置的元素不相同,则将其加入到新数组中,同时慢指针 slow 增加。

正解:

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        int n = nums.size();

        // 打印 n 范围内所有的元素
        if(n <= 2) return n;

        int slow = 2;
        for(int fast = 2; fast < n; ++fast){

            // 当前元素要和它左边第 2 个元素进行比较
            if(nums[fast] != nums[slow - 2]){
               nums[slow] = nums[fast];
               ++slow;
            }
        }
        return slow;
    }
};

        还有一点值得注意:自增符 ++ 可以放在 nums[slow++] 中去写

  •         nums[slow++] —— 先用 nums[slow] 接收新值, slow 再加 1;
  •           nums[++slow] —— slow 先加 1,再用 nums[slow + 1] 接收新值;

6、多数元素

题目描述:

解法:       

        使用哈希映射(HashMap)来存储每个元素以及出现的次数。对于哈希映射中的每个键值对,键表示一个元素值表示该元素出现的次数

        对于数组的遍历,使用 C++11 中新增的 增强型 for 循环 

for(int num: nums)

        num 会遍历容器 nums 中的所有元素,这样可以很方便地记录每个元素的值和它们出现的次数

class Solution {
public:
    int majorityElement(vector<int>& nums) {
         
         // 利用映射类型的哈希结构,键存元素,值存出现次数
         unordered_map<int, int> counts;

         // 用来记录当前出现次数最多的元素以及它的出现次数
         int Cur = 0;
         int CurCnt = 0;
         
         for(int num: nums){
            ++counts[num];

            // 擂台赛
            if(counts[num] > CurCnt){
                Cur = num;  // 更新主要元素为当前元素
                CurCnt = counts[num];  // 更新出现次数最多的元素的出现次数
            }
         }
         return Cur;
    }
};

        使用 if 语句进行元素间的擂台赛,决出最终出现次数最多的元素,这个操作可以省去遍历哈希表找出值最大者这一过程

7、轮转数组

题目描述:

解法:

        首先给出第一次 AC 了99%测试用例的版本,方便记录我忽略了这道题[最关键的信息]。        

        这道题的思路是容易想到的,说是“轮转”数组,其实用[滚动]数组描述更为贴切,每次从末尾元素开始,末尾元素滚至 下标0 的位置,剩余所有元素都要向右移动。

        此时应注意的时,如果从头向尾移动,那么下标较大的元素会被下标较小的元素覆盖掉,所以应该从后向前移动每个元素,当除了原本的末尾元素还未就位,其余元素都已完成了位置的轮转,而且下标0 的位置被空出来了,把末尾元素插进去就好了,那么末尾元素为什么不会被覆盖掉呢?

        我们可以提前使用一个临时变量来存储末尾元素

class Solution {
public:
    void rotate(vector<int>& nums, int k) {
        
        int n = nums.size();
        while(k--){

             int tmp = nums[n - 1]; // 将尾元素放在临时变量中
            for(int i = n - 1; i > 0; i--){
                nums[i] = nums[i - 1];
            }
            nums[0] = tmp;
        }
    }
};

        但是这种方法的时间复杂度是 O(k*n),k是旋转步数,n是数组的长度,在最后一个测试用例上超时了。

正解:

        使用额外的等长数组来存储轮转后的元素,并将这个数组中的元素依次赋值回原数组,这个解法的时间复杂度只有O(n),因为只需要遍历1次数组

class Solution {
public:
    void rotate(vector<int>& nums, int k) {

        // 使用额外的等长数组
        int n = nums.size();
        vector<int> newNums(n);
        
        for(int i = 0; i < n; ++i){
            newNums[(i + k) % n] = nums[i];
        }

        for(int i = 0; i < n; ++i){
            nums[i] = newNums[i];
        }
    }
};

        只是在判断元素的新下标时的公式不好想

(i  + k) % n

        总觉得有点像哈希冲突时的解法,希望下次写这题时我还记得这个公式

8、罗马数字转整数

题目描述:

解法:

        这题的唬人程度根本不是简单题的难度!官方给的描述太冗杂了,实际上就是

        “如果当前字符的值小于其后面一个字符的值(即罗马数字规则中小数在大数左边表示减法),则将该值从结果中减去;否则将该值加到结果中。”

class Solution {
public:
    int romanToInt(string s) {

        // 标准的键值对形式,使用映射类型的哈希表结构存储
        unordered_map<char, int> Roma = {
            {'I', 1},
            {'V', 5},
            {'X', 10},
            {'L', 50},
            {'C', 100},
            {'D', 500}, 
            {'M', 1000},
        };

        int n = s.length();
        int ans = 0;

        for(int i = 0; i < n; ++i){

            if(i < n - 1 && Roma[s[i]] < Roma[s[i + 1]]){
                ans -= Roma[s[i]];  // 小数在大数左边表示减法
            }else{
                ans += Roma[s[i]];
            }
        }
        return ans;
    }
};

        对于官方提供的罗马数字和阿拉伯数字之间的对应关系,应该很自然地想到使用哈希表来存储它

        if 语句的判断条件为:确保 i 不是最后一个元素,因为最后一个元素没有更后的元素和它比较了

        当前元素如果小于后一个元素,则进行减法,否则进行加法

9、买卖股票的最佳时机Ⅱ

题目描述:

解法:

        与“买卖股票的最佳时机”不同的是,此题可以买卖多支股票

        找到最小的数了,在什么时候卖掉呢?示例1中表示,想要获得最大利润未必要在后续数中挑一个最大的去卖掉,按这个思路想的话会有以下问题:

                1.如何判断买卖一支股票和买卖多支股票之间的利润大小?

                2.刚开始可以找到值最小的那支股票,并假设后面可以卖掉;那么第二支股票呢?而且还面临一个问题:对于第一支股票而言,如果选择后续利润最大的一天卖掉,那么这一天之前价格比较低的第二支股票会因为过了买卖时间而无法进行交易了

        总之想要用一般的方法去解决这个问题超出了我愚笨大脑的极限,所以总结了使用贪心解决问题的算法

        (代码随想录)贪心算法:

                假设以下数组

如果想到其实最终利润是可以分解的,那么本题就很容易了!

如何分解呢?

假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。

相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。

此时就是把利润分解为每天为单位的维度,而不是从 0 天到第 3 天整体去考虑!

那么根据 prices 可以得到每天的利润序列:(prices[i] - prices[i - 1]).....(prices[1] - prices[0])。

从图中可以发现,其实我们需要收集每天的正利润就可以,收集正利润的区间,就是股票买卖的区间,而我们只需要关注最终利润,不需要记录区间

那么只收集正利润就是贪心所贪的地方!

局部最优:收集每天的正利润,全局最优:求得最大利润

局部最优可以推出全局最优,找不出反例,试一试贪心!

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        
        int profit = 0;
        for(int i = 1; i < prices.size(); ++i){
            profit += max(prices[i] - prices[i - 1], 0);

        }
        return profit;
    }
};

10、最后一个单词的长度

题目描述:

        

解法:

         字符串是若干字符组成的有限序列,也可以理解为是一个字符数组。从后往前遍历,先着手于测试用例1和3,如代码中的第二个while循环,可以统计最后一个单词的长度。

        此时不加第一个while循环对串尾进行处理的话,index进不去第二个while循环,从而无法index--,会直接return cnt = 0。逻辑就是先处理正常情况下的字符串,而对于串尾是空格的字符串,再去执行处理方案。

class Solution {
public:
    int lengthOfLastWord(string s) {
        
        int cnt = 0;
        int index = s.size() - 1;

        // 处理串尾是空格时的情况
        while(s[index] == ' ') index--;
        
        while(index >= 0 && s[index] != ' '){
            cnt++;
            index--;
        }
        return cnt;
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/506949.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥备赛——贪心

题干 AC Code n, w = map(int, input().split()) # n种类, w核载重 a = [] # [[weight1, value1], [weight2, value2], ...] for _ in range(n):a.append(list(map(int, input().split()))) a.sort(key=lambda x: x[1] / x[0], reverse=True)maxVal = 0for i in a:if i[0…

亮数据Bright Data,引领高效数据采集新体验

随着互联网和大数据的日益普及&#xff0c;我们对于高速、安全和无限畅通的网络体验追求越发迫切&#xff0c;随之而来的网络安全和隐私保护变得越来越重要。IP代理作为一种实用的代理工具&#xff0c;可以高效地帮我们实现网络数据采集&#xff0c;有效解决网络安全问题&#…

大数据量查询语句优化

测试单表模糊查询&#xff0c;符合条件的数量为&#xff1a; -- 查看总共有多少条数据 select count(0) from "REGISTER_HOUSE_INFO" where SEAT_NAME like %1% ;未优化&#xff1a;测试单表模糊查询分页&#xff0c;符合条件的数据为&#xff1a; select * from …

单词精灵,Android 记单词 app 开发

使用 Android Studio 开发了一款 记单词 app —— 《单词精灵》 关键词&#xff1a;单词精灵 A. 项目描述 《单词精灵》是一款专为Android平台设计的单机记单词应用。该应用旨在帮助用户系统、高效地扩展词汇量&#xff0c;提升英语水平。应用内置丰富的词库和记忆方法&#…

C++AVL树拓展之红黑树原理及源码模拟

前言&#xff1a;我们之前已经从零开始掌握AVL树http://t.csdnimg.cn/LaVCChttp://t.csdnimg.cn/LaVCC 现在我们将继续学习红黑树的原理并且实现插入等功能&#xff0c;学习本章的前提要求是掌握排序二叉树和AVL树&#xff0c;本章不再提及一些基础知识&#xff0c;防止本文结…

LeetCode-560. 和为 K 的子数组【数组 哈希表 前缀和】

LeetCode-560. 和为 K 的子数组【数组 哈希表 前缀和】 题目描述&#xff1a;解题思路一&#xff1a;一边算前缀和一边统计。这里用哈希表统计前缀和出现的次数&#xff0c;那么和为k的子数组的个数就是当前前缀和-k的个数&#xff0c;即preSums[presum - k]。画个图表述就是&a…

sparksql执行流程

1. SparkSQL的自动优化 我们前面的文章已经说过spark RDD定义好后&#xff0c;执行经过DAG sechduler划分号内存管道、逻辑任务&#xff0c;然后经由task scheduler来分配到具体worker来管理运行&#xff0c;RDD的运行会完全按照开发者的代码执行 如果开发者水平有限&#xff…

一文了解JAVA的常用API

目录 常用kpimathSystemRuntimeObjectObjectsBigIntegerBigDecima正则表达式包装类 常用kpi 学习目的&#xff1a; 了解类名和类的作用养成查阅api文档的习惯 math 工具类。因为是工具类&#xff0c;因此直接通过类名.方法名(形参)即可直接调用 abs&#xff1a;获取参数绝对…

Spring如何进行事务管理?什么是面向切面编程?

喜欢就点击上方关注我们吧&#xff01; 本篇将带你快速了解Spring事务管理以及面向切面编程(AOP)相关知识。 一、事务 1、概述 1&#xff09;事务是一组操作的集合&#xff0c;是一个不可分割的工作单位&#xff0c;这些操作要么同时成功&#xff0c;要么同时失败。 2&#xff…

八股 -- C#

面向对象 &#xff08;三大特性&#xff09; 三大特性目的是为了提供更好的代码组织、可维护性、扩展性和重用性 C#基础——面向对象 - 知乎 (zhihu.com) 封装 理解&#xff1a; 你不需要了解这个方法里面写了什么代码&#xff0c;你只需要了解这个方法能够给你返回什么数据&…

矩阵乘法优化:GEMM中如何将大矩阵切割成小矩阵

论文自然还是 Anatomy of High-Performance Matrix Multiplication。 如何拆分 一个矩阵乘法有 6 种拆分方式&#xff0c;其中对 row-major 效率最高的是&#xff1a; 第一次拆分 先做第一次拆分&#xff0c;取 A 的 kc 列&#xff08;PanelA&#xff09;和 B 的 kc 行&…

基于 7 大城市实景数据,清华大学团队开源 GPD 模型

城市&#xff0c;是人们安居乐业的故土&#xff0c;是政府开展经济建设的基石&#xff0c;承载着细腻的人文情怀与宏伟的国家发展脉络。长期以来&#xff0c;管理者一直在探寻更加高效、科学的城市治理方法&#xff0c;解决不同地区资源供给不平衡、交通拥挤、人口流失等问题。…

Qt项目通过.pri文件将众多文件按功能模块分类显示,开发大型项目必备

Chapter1 Qt项目通过.pri文件将众多文件按功能模块分类显示&#xff0c;开发大型项目必备 Chapter2 在Qt项目中添加pri文件 原文链接&#xff1a;在Qt项目中添加pri文件_qtpri-CSDN博客 前言 一般我们创建Qt项目工程的时候&#xff0c;都是直接把所有的项目&#xff0c;头文…

Chatopera 云服务的智能问答引擎实现原理,如何融合 #聊天机器人 技术 #Chatbot #AI #NLP

观看视频 Bilibili: https://www.bilibili.com/video/BV1pZ421q7EH/YouTube: https://www.youtube.com/watch?vx0d1_0HQa8o 内容大纲 提前在浏览器打开网址&#xff1a; Chatopera 云服务&#xff1a;https://bot.chatopera.comChatopera 入门教程&#xff1a;https://dwz…

微机原理-基于8086电压报警器系统仿真设计

**单片机设计介绍&#xff0c;微机原理-基于8086电压报警器系统仿真设计 文章目录 一 概要二、功能设计设计思路 三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于8086的电压报警器系统仿真设计概要主要涉及到系统的整体架构设计、硬件组成、软件逻辑设计以及仿真环境…

【智能算法】黄金正弦算法(GSA)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2017年&#xff0c;Tanyildizi等人受到正弦函数单位圆内扫描启发&#xff0c;提出了黄金正弦算法&#xff08;Golden Sine Algorithm, GSA&#xff09;。 2.算法原理 2.1算法思想 GSA来源于正弦函…

前端学习<二>CSS基础——14-CSS3属性详解:Web字体

前言 开发人员可以为自已的网页指定特殊的字体&#xff08;将指定字体提前下载到站点中&#xff09;&#xff0c;无需考虑用户电脑上是否安装了此特殊字体。从此&#xff0c;把特殊字体处理成图片的方式便成为了过去。 支持程度比较好&#xff0c;甚至 IE 低版本的浏览器也能…

C语言内存函数(超详解)

乐观学习&#xff0c;乐观生活&#xff0c;才能不断前进啊&#xff01;&#xff01;&#xff01; 我的主页&#xff1a;optimistic_chen 我的专栏&#xff1a;c语言 点击主页&#xff1a;optimistic_chen和专栏&#xff1a;c语言&#xff0c; 创作不易&#xff0c;大佬们点赞鼓…

安全用电监控系统在工厂的研究与应用论述

摘 要&#xff1a;随着社会时代的发展&#xff0c;人们的安全意识越来越强烈&#xff0c;在人们生活和工作中离不开各种用电设备&#xff0c;用电设备的安全使用是保障人们生命安全的重要内容。工厂因自身厂内工作环境的特殊性&#xff0c;用电设备的种类多且复杂&#xff0c;如…

【数据结构与算法初阶(c语言)】插入排序、希尔排序、选择排序、堆排序、冒泡排序、快速排序、归并排序、计数排序-全梳理(万字详解,干货满满,建议三连收藏)

目录 1.排序的概念及其运用 1.1排序的概念 1.2排序运用 1.3常见的排序算法 2.插入排序 2.1 原理演示&#xff1a;​编辑 2.2 算法实现 2.3 算法的时间复杂度和空间复杂度分析 3.希尔排序 3.1算法思想 3.2原理演示 3.3代码实现 3.4希尔算法的时间复杂度 4.冒泡排序 4.1冒泡排…