代码随想录算法训练营第六天|字符串 & 栈 & 队列

字符串

字符串是我的弱点,主要是函数用的不是很熟练

注意因为字符串是左闭右开区间,而且字符串末尾还有一个"/0",所以我们在reverse的时候是 [s.begin(),s.begin() + len]

其中len是string的长度,假如string的长度是6,元素索引就是[0,1,2,3,4,5],reverse的范围就是[0,6] (因为最后有“/0”)

同理substr函数也是这么用

但是要注意:

reverse(s.begin()+1,s.end())

s.substr(0,0+len)

使用方式和传入参数都是不一样的

Leecode28. 找出字符串中第一个匹配项的下标 & KMP算法

KMP算法用来在文本串中匹配模式串的

什么是最长相等前后缀,举个例子:
比如a一个元素,没有前缀也没有后缀的
比如aa,那么有前缀a(第一个a),也有后缀a(第二个a),所以最长相等前后缀是1
比如aba,看样子好像有。最长前缀是ab,最长后缀是ba,显然不相等,还有次长前缀a,次长后缀a,那么最长相等前后缀就是1
比如aab,ab和aa不等,a和b也不等,所以最长相等前后缀是0
···

用KMP之前我们需要建立一张前缀表(next数组),这样方便我们在比较文本串和模式串的时候遇见不匹配的情况下方便模式串中的指针回退

我们在KMP算法中把当前子串的最长相等前后缀记录到末尾字符的next数组中

很简单,直接上代码

class Solution {
public:
    //定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置
    void getNext(int *next,const string& s)
    {
        // 首先进行初始化,也就是将j(j指向的是模式串)也就是将j初始化为0,显然第一个字符并没有前后缀
        int j = 0;
        next[0] = j;
        for(int i=1;i<s.size();i++) // 开始同时比较一个串中的字符,记录每个位置的最长相等前后缀
        {
            // 那么若是不匹配呢,那只能说和刚开始(i-1)的位置和i不匹配,还要往回退看看前面有无和i相等的元素
            // 一定注意while循环中与条件中一定要把索引条件写在最前面
            while(j>0 && s[i] != s[j]) j = next[j-1]; // j是索引,若是不匹配,应该往回退


            // 若是i和j匹配,那么j就++,并且在循环末尾应该让当前位置的next数组的值等于j
            if(s[i] == s[j]) j++;

            next[i] = j;
        }
    }
    //定义两个下标j 指向模式串起始位置,i指向文本串起始位置。
    int strStr(string haystack, string needle) {
        // 开始的时候应该定义next数组并且用上面的函数给next数组填充值
        int next[needle.size()];
        getNext(next,needle);

        int j = 0;
        //这次开始从头比较,其实比较函数中i和j的作用和用暴力法做字符串匹配时的i和j的作用是一样的,分别指向文本串和模式串的起始位置
        for(int i = 0;i<haystack.size();i++)
        {
            // 如果不匹配,那么一直进行回退,所以还是while
            while(j>0 && haystack[i] != needle[j]) j = next[j-1];

            // 如果匹配,那么j的位置往后加
            if(haystack[i] == needle[j]) j++;

            // 注意这里的下标一定是needle.size(),因为若是needle.size()-1,那么此时指向的是最后一个元素,还没有确定匹配与否
            if(j == needle.size()) {return i-needle.size()+1;} //但是若是匹配此时i从匹配位置开始刚好走了needle.size()步,因为输出索引所以+1
        }
        return -1;
    }
};

Leecode459. 重复的子字符串

证明:如果文本串是由多个完全相等的子串构成,可以证明子串的长度就等于串的长度减去最长相等前后缀的长度,有了这个性质,那么我们需要考虑一下如何利用这个性质解题

  1. 既然最长相等前后缀的长度是文本串(重复子串)的长度减去子串的长度,那么不满足由重复子串构成的的文本串的最长相等前后缀的长度就一定不是文本串的长度减去子串的长度,换言之文本串的长度减去最长相等前后缀的长度就一定不是子串的长度

  2. 若是重复子串,其模子串的长度是等于0的,最长相等前后缀的长度模子串也一定是等于0的。满足这两条就可以判定文本串是由重复的子字符串构成的。反证法:若文本串不是由重复的子字符串构成的也同样满足这两条,那么就违背了1,所以由反证法我们可以得到验证文本串的条件

  3. 首先由文本串的长度减去最长相等前后缀的长度得到子串的长度(这里我们假设文本串都是重复的子字符串构成的,那么最长相等前后缀的长度一定就是文本串末尾元素的next数组的值),然后用文本串模子串,用最长相等前后缀模子串,若都为0,那么可得文本串是由重复的子字符串构成的

class Solution {
public:
    void Getnext(int *next,const string & s)
        {
            int j = 0;
            next[j] = 0;
            for(int i=1;i<s.size();i++)
            {
                while(j>0 && s[j]!=s[i]) j = next[j-1];

                if(s[j] == s[i]) j++;

                next[i] = j;    
            }
        }
    bool repeatedSubstringPattern(string s) {
        // 先把next数组给写出来,然后再考虑其他的东西
        int next[s.size()];
        // 然后定义出整个数组的next,并且调用函数计算出next数组
        Getnext(next,s);
        // 其实不满足重复子字符串,最后一个字符的next数组也是可能有值的
        int size = s.size();
        int num = size - next[size - 1];
        if(s.size() % num ==0 && next[size - 1]!=0 && next[size - 1]%num==0) return true; // 不等于0的条件一定要写出来,不然是非法的
        return false;
    }
}; 

Leecode344.反转字符串

链接:https://leetcode.cn/problems/reverse-string/

class Solution {
public:
    void reverseString(vector<char>& s) {
        int left = 0,right = s.size()-1;
        while(left < right) {swap(s[left++],s[right--]);}
    }
};

Leecode541. 反转字符串 II

class Solution {
public:
    string reverseStr(string s, int k) {
        // 梳理一下思路:从头开始计数,每次到了2k就翻转前面k个字符
        // 若是剩余字符数量不足2k但是大于等于k,那么就翻转前k个字符
        // 所以究竟是怎么翻转的?翻转k个,隔k个,然后再翻转

        int size = s.size();
        int index = 0;
        while(1)
        {   // 刚开始index = 0,index + 3就是最后一个元素
            if(index + (2 * k)> size) // 剩余不足2k
            {
                if(index + (k) <= size) // 就将剩余元素全部翻转
                reverse(s.begin() + index , s.begin() + index + k);  
                else reverse(s.begin() + index , s.end());  

                break;
            }
            else // 剩余元素大于等于2k 
            {
                reverse(s.begin() + index , s.begin() + (index + k));
                index += (2 * k);  
            }
        }
        return s;
    }
};

剑指 Offer 05. 替换空格

// 填充类题目,属实是一道好题
class Solution {
public:
    string replaceSpace(string s) {
        // 一般想到的思路其实是多开辟一个数组空间
        // 但是我们可以在原数组的基础上修改,这样就不用开辟多余的空间

        // 首先计算出s中空格的个数。然后用resize函数对原先的s进行扩容
        int size = s.size();
        int count = 0;
        for(int i = 0;i<size;i++) {if(s[i] == ' ') count++;}

        s.resize(size + count * 2);
        // 从后往前移动指针,左指针指向原数组的末尾,右指针指向新数组的末尾
        // 左指针每次移动一位,若是遇到了空格,右指针往左移动三位填充
        // 直到左指针小于0 为止
        int left = size-1;
        int right = s.size()-1;
        while(left >= 0)
        {
            if(s[left] != ' ') {s[right] = s[left]; right--;}
            else 
            {
                s[right] = '0';
                s[right - 1] = '2';
                s[right - 2] = '%';
                right-=3;
            }
            left--;
        }
        return s;
    }
};

Leecode151. 反转字符串中的单词

此题明显分两步走,首先是去除多余的空格

  1. 第一个单词前多余的空格
  2. 两个单词之间多余的空格
  3. 最后一个单词后多余的空格

我刚开始的时候考虑直接用一个函数完成去除空格的操作,但是由于操作1和操作23不是一个逻辑,所以操作1和操作23我们分成两步完成

先说一下为什么操作2和操作3可以用一个函数完成

"hello world ",如左侧字符串,其实是比较规律的,都是先出现字符,然后出现空格

但是如果单词前有空格,就很难一步实现

去掉多余的空格之后,我们将字符串翻转,然后再取出其中的单个单词再做一次翻转操作,就可以完成题目要求

class Solution {
public:
void reverseString(string& s,int left,int right) {
	while (left < right) { swap(s[left++], s[right--]); }
}
string reverseWords(string s) {
	int left = 0, right = 0;
	int size = s.size() - 1;
	// 开始的时候定义左右指针,右指针先动,
	// 分三步去掉字符串中的空格
	// 别想着写一个函数就去掉,这明显不是一套逻辑
	while (1)
	{
		if (s[right] == ' ') right++;
		else break;
	}
	left = right;

	//开始去掉中间元素
	int start = left;
	int flag = 0;
	while (right <= size)
	{
		if (s[right] != ' ')
		{
			if (!flag) { s[left++] = s[right]; }
			else { flag = 0; s[left++] = ' '; s[left++] = s[right]; }
		}
		else { flag = 1; }
		right++;
	}
	s = s.substr(start, left - start); // 这里也是大坑,去除前面元素之后我们将left作为字符串的起点并赋值给start,之后再操作left令其指向去除空格后的字符串末尾的位置,因此整个字符串应该截取的范围是[start,left]

	// 然后先对s进行reverse,之后再对每一个单词进行reverse
	reverse(s.begin(), s.end());

	int l_index = 0, r_index = 0;

	while (r_index < s.size())
	{
		while (s[r_index] != ' ' && r_index<s.size() - 1) r_index++;
		// 两种情况,若是此时右指针指向的是空格
		if (s[r_index] == ' ')
		{
			reverseString(s, l_index, r_index - 1);
            // 因为去除空格后字符串的末尾一定不是空格,所以在翻转前面一个单词后我们将左右指针都指向空格前面的位置
			l_index = r_index + 1;
			r_index++;
		}
		else if (r_index == s.size()-1) // 已经走到结尾,翻转后退出就OK
		{
			reverseString(s, l_index, r_index);
			return s;
		}
	}
	return s;
}
};

剑指Offer58-II.左旋转字符串

翻转三次可以达到目标,像是规律类的一种题目

class Solution {
public:
    // 翻转的时候要牢记reverse操作是左闭右开的
    string reverseLeftWords(string s, int n) {
        reverse(s.begin(),s.end());
        reverse(s.begin(),s.end() - n);
        reverse(s.end() - n,s.end());
        return s;
    }
};

栈与队列

Leecode232. 用栈实现队列

相当于是熟练stl库的操作吧

用两个栈实现队列先进先出的操作,pop元素的时候把储存元素的栈的元素都push进另外一个栈中,然后pop

class MyQueue {
public:
    stack<int> In;
    stack<int> Out; 
    MyQueue() {
        // 这个不用写
    }
    void push(int x) {
        // push操作是push到In中
        In.push(x);
    }
    
    int pop() {
        if(Out.empty())
        {
            // 将In中的元素全都转移到Out中去
            while(!In.empty())
            {
                // 注意pop()操作是没有返回值的,要得到值只能用top()操作

                int num = In.top();
                In.pop();
                Out.push(num);
            }
        }
        int res = Out.top();
        Out.pop();
        return res;
    }
    
    int peek() {
        int res = this->pop();
        Out.push(res);
        return res;
    }
    
    bool empty() {
        if(In.empty() && Out.empty())
        return true;

        return false;
    }
};

Leecode225. 用队列实现栈

也简单,用一个队列就可以实现栈,因为栈是先进后出,所以要用先进先出实现先进后出,就要将非目标全都pop出去然后再push进来,就轮到我们要pop的那个元素了

class MyStack {
public:
    queue<int> p;
    MyStack() {

    }
    
    void push(int x) {
        p.push(x);
    }
    
    int pop() {
        // 先pop出size - 1个元素,pop出一个就push进去一个,最后pop出的元素就是我们需要的元素
        int size = p.size();
        size --;
        while(size--)
        {
            int num = p.front();
            p.pop();
            p.push(num);
        }
        int val = p.front();
        p.pop();
        return val;
    }
    
    int top() {
        int val = this->pop();
        p.push(val);
        return val;
    }
    
    bool empty() {
        if(p.empty()) return true;
        return false;
    }   
};

Leecode20. 有效的括号

// 首先梳理一下思路
// 利用栈,遍历字符串,将左侧的括号全都加入到栈中,若是右侧的括号就单独判断其与栈中的括号是不是匹配
// 要是遇见右括号,就从栈中弹出元素,并且判断这两个括号是不是匹配
// 若是栈中弹出的括号与当前遍历到的括号不匹配,那么false
// 如果最后栈空,那么false

// 关键就在于找一种办法能够判断右侧的括号是与左侧匹配的
// 可以写一个check()函数
class Solution {
public:
    stack<char> st;
    bool check(char s1,char s2)
    {
        if(s1 == '(' && s2 == ')') return true;
        else if(s1 == '{' && s2 == '}') return true;
        else if(s1 == '[' && s2 == ']') return true;
        return false;
    }
    bool isValid(string s) {
        for(int i=0;i<s.size();i++)
        {
            if(s[i] == '(' || s[i]=='[' || s[i] == '{') st.push(s[i]);
            else
            {
                // 不是左括号那么就是右括号,直接匹配,不匹配返回false
                // 若是栈为空,那肯定也是false
                if(st.empty() || !check(st.top(),s[i])) return false;
                else st.pop();
            }
        }
        if(st.empty()) return true;
        return false;
    }
};

Leecode1047. 删除字符串中的所有相邻重复项

class Solution {
public:
    // 水题
    // 思考一下:
    // 栈是空的时候,我们就直接将其push进栈
    // 若栈不为空,我们比较遍历到的元素和栈顶的元素,若是相同,那么直接pop出栈顶的元素
    // 最后我们直接用字符串接受栈中的元素
    // 然后我们用string接受栈弹出的元素
    // 最后reverse输出即可(或者用队列接受)
    stack<char> st;
    string removeDuplicates(string s) {
        int size = s.size();
        for(int i=0;i<size;i++)
        {
            if(st.empty()) st.push(s[i]);
            else 
            {
                if(s[i] == st.top())
                st.pop();
                else st.push(s[i]);
            }                       
        }
        string res = "";

        while(1) 
        {
            if(st.empty()) break;
            res += st.top();
            st.pop();
        }

        reverse(res.begin(),res.end());
        return res;
    }   
};

Leecode150. 逆波兰表达式求值

class Solution {
public:
    bool check_is_num(string c)
    {
        if(c == "+" || c=="-" || c=="*" || c=="/") return false;
        return true; 
    }
    long long cal(string number1,string number2,string op) // 又是不开long long见祖宗的好题
    {
        long long num1 = atoi(number1.c_str());
        long long num2 = atoi(number2.c_str());
        if(op == "+") return (long long)(num1 + num2);
        else if(op == "-") return (long long)(num1 - num2);
        else if(op == "*") return (long long)(num1 * num2);
        return (long long)(num1 / num2);// 保留整数
    }
    stack<string> st;
    int evalRPN(vector<string>& tokens) {
        // 什么水题,解决方法都在下面告诉你了
        // 首先开一个栈,然后遍历数组
        // 遇见数就直接加入栈中,遇见操作符就取出栈顶的两个元素操作,然后push入栈
        int size = tokens.size();
        for(auto i : tokens)
        {
            if(check_is_num(i)) st.push(i);
            else 
            {
                string num1 = st.top(); st.pop(); // 注意先出栈的后操作,在操作符的右边,因此是第二个参数
                string num2 = st.top(); st.pop();
                long long num = cal(num2,num1,i);

                string res = to_string(num);
                st.push(res);
            } 
        }
        return atoi(st.top().c_str());
    }
};
// 主要注意两点:1.atoi(x.c_str())将字符串x转化成int类型 2.to_string(num)将数字转化成字符串
// 中等题以上看数据返回,不开long long见祖宗  

Leecode239. 滑动窗口最大值

其实就是维护这样一种数据结构

如图所示,存放在队列中的数一定是递减的,并且一定要把最大数放在最前面

在这里插入图片描述

所以代码逻辑也就不难理解了:

  1. 每个数进入队列之后都会和队尾的元素比较:如果自己是第三大,那么仅保留前面两个数;如果自己是第二大,那么仅保留前面一个数,若自己最大,那么除了自己全都清除;如果自己比队尾还小,那赶紧走;如果小于等于队尾那么就留下来——如何知道自己是第几大呢?不断和队尾比较就完事了
  2. 弹出元素的时候,因为单调队列维护了最大值也维护了位置,虽然滑动窗口移动的时候删除的不一定是队列中的元素,但是一定删除的是最大值(注意相同大小的元素一定要么都在队列,一定都不在队列),所以每次删除的时候比较当前的值和对头值,若是相等就删除,就是这么简单
  3. 至于最大元素,直接找队头就完事
class Solution {
private:
    class Myqueue
    {
        public:   
        deque<int> que;
        // 然后我们实现三种操作:分别是push(),pop(),还有search()
        void push(int x) // 加入元素要求保证:若其比队列中元素大,队列中元素就要pop出去
        {   
            if(que.empty()) que.push_back(x);
            else 
            {   // 即使在else里面循环中不等于空的条件也还是要写,就是这么细
                while(!que.empty() && x > que.back()) que.pop_back(); // 这里错了,应该是比较x和que.back()
                que.push_back(x);
            }
        }

        void pop(int x) // pop元素的话是要传入值的,这是和普通队列不一样的一点
        {
            if(que.front() == x && !que.empty()) que.pop_front();
        }

        int search()
        {
            return que.front();
        }

    };
    public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        // 然后就比较好做了,定义一个索引,刚开始的时候push进去 0-index 中的所有元素
        Myqueue que;
        vector<int> res;
        // 开始的时候把数字全都装到deque中去,然后移动一位push一个加进去一个
        for (int i = 0; i < k; i++) { // 先将前k的元素放进队列
            que.push(nums[i]);
        }
        
        res.push_back(que.search());

        for (int i = k; i < nums.size(); i++) {
            que.pop(nums[i - k]); // 滑动窗口移除最前面元素
            que.push(nums[i]); // 滑动窗口前加入最后面的元素
            res.push_back(que.search()); // 记录对应的最大值
        }
        return res;
    }
};

Leecode347. 前 K 个高频元素

首先用map存储数字和其出现的次数,然后放到小根堆里面,自定义排序规则,基本上就是用模板就无压力的一道题

class Solution {
public:
    // 重载排序规则
    class mycomparison {
    public:
        bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) 
        {
            return lhs.second > rhs.second;
        }
    };
    vector<int> topKFrequent(vector<int>& nums, int k) 
    {
        // 用map存储出现的数字和其出现的次数,second存储的是每个数字的出现次数,上面我们已经定义好了小根堆的排序规则,直接将map中的元素push进去就好
        unordered_map<int, int> map; 
        for (int i = 0; i < nums.size(); i++) map[nums[i]]++;

        priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pri_que;

        for (auto it = map.begin(); it != map.end(); it++) 
        {
            pri_que.push(*it);
            if (pri_que.size() > k) pri_que.pop();
        }

        // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组
        vector<int> res(k);
        for (int i = k - 1; i >= 0; i--) 
        {
            res[i] = pri_que.top().first;
            pri_que.pop();
        }
        return res;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
代码随想录算法训练营是一个优质的学习和讨论平台,提供了丰富的算法训练内容和讨论交流机会。在训练营中,学员们可以通过观看视频讲解来学习算法知识,并根据讲解内容进行刷题练习。此外,训练营还提供了刷题建议,例如先看视频、了解自己所使用的编程语言、使用日志等方法来提高刷题效果和语言掌握程度。 训练营中的讨论内容非常丰富,涵盖了各种算法知识点和解题方法。例如,在第14天的训练营中,讲解了二叉树的理论基础、递归遍历、迭代遍历和统一遍历的内容。此外,在讨论中还分享了相关的博客文章和配图,帮助学员更好地理解和掌握二叉树的遍历方法。 训练营还提供了每日的讨论知识点,例如在第15天的讨论中,介绍了层序遍历的方法和使用队列来模拟一层一层遍历的效果。在第16天的讨论中,重点讨论了如何进行调试(debug)的方法,认为掌握调试技巧可以帮助学员更好地解决问题和写出正确的算法代码。 总之,代码随想录算法训练营是一个提供优质学习和讨论环境的平台,可以帮助学员系统地学习算法知识,并提供了丰富的讨论内容和刷题建议来提高算法编程能力。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [代码随想录算法训练营每日精华](https://blog.csdn.net/weixin_38556197/article/details/128462133)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值