《算法竞赛》 第一章基础数据结构

链表

C++ STL - list

题目:P1996 约瑟夫问题 - 洛谷题库

#include<iostream>
#include<list>
using namespace std;

int main(){
    int n, m;
    cin >> n >> m;
    list<int>node;
    //创建链表
    for(int i = 1; i <= n; i ++){
        node.push_back(i);
    }
    //通过迭代器it去遍历链表
    list<int>:: iterator it = node.begin();
    //模拟约瑟夫环的过程
    while(node.size() > 1){
        for(int i = 1; i < m; i ++){
            it ++;
            if(it == node.end()){
                it = node.begin();
            }
        }
        //输出第m个元素(当前要删除的元素)
        cout << *it << " ";
        //更新迭代器指向下一个元素,并删除当前元素
        list<int>:: iterator next = ++it;
        if(next == node.end())
            next = node.begin();
        node.erase(--it);
        it = next;
    }
    //while循环结束,此时只剩下一个元素
    cout << * it;
    return 0;
}

队列

队列是先进先出,从队尾添加元素,从队头删除元素

C++ STL - queue

题目链接:P1540 [NOIP2010 提高组] 机器翻译 - 洛谷题库

#include<iostream>
#include<queue>
using namespace std;
int Hash[1003] = {0}; //Hash[i] == 0 表示i不在内存中,否则i在内存中
queue<int> mem;//用队列模拟内存
int main(){
    int n, m;
    cin >> m >> n;
    int cnt = 0;//统计次数
    while(n--){
        int en;
        cin >> en;//输入一个英文单词
        if(!Hash[en]){
            ++ cnt;// 内存中没有,去外存中查找,查找次数加1
            mem.push(en);
            Hash[en] = 1;// 内存中有这个单词
            while(mem.size() > m){//队列满了
                Hash[mem.front()] = 0;
                mem.pop();// 从队头去掉
            }
        }
    }
    printf("%d\n", cnt);
    return 0;
}

手写循环队列

题目链接:P1540 [NOIP2010 提高组] 机器翻译 - 洛谷题库

竞赛中一般采用静态分配

#include<iostream>
using namespace std;
#define N 1003 // 队列大小
int Hash[N];  //用哈希检查内存中有没有单词
//分配静态空间
struct myQueue{
    int data[N];
    int head, rear;// 队头、队尾
    bool init(){
        head = rear = 0;
        return  true;
    }
    int size(){ // 返回队列的长度
        return (rear - head + N) % N;
    }
    bool empty(){ // 队列的判空
        if(size() == 0){
            return true;
        }else{
            return false;
        }
    }
    bool push(int e){ //队列的插入
        //如果队满,则插入失败
        if((rear + 1) % N == head){ //队满的判断
            return false;
        }
        data[rear] = e;
        rear = (rear + 1) % N;
        return true;
    }
    bool pop(int &e){ // 删除队头元素,结果用e返回
        if(head == rear){
            return false;
        }
        e = data[head];
        head = (head + 1) % N;
        return true;
    }
    int front(){ //返回队首元素,但是不删除
        return data[head];
    }
}Q;
int main(){
    Q.init(); // 初始化队列
    int m, n;
    cin >> m >> n;
    int cnt = 0;
    while(n -- ){
        int en;
        cin >> en;
        if(!Hash[en]){ //内存中没有这个单词
            ++ cnt;// 先去外存
            Q.push(en);
            Hash[en] = 1;
            while(Q.size() > m){
                int tmp;
                Q.pop(tmp);
                Hash[tmp] = 0;
            }
        }
    }
    cout << cnt << endl;
    return 0;
}

双端队列和单调队列

双端队列和单调队列的概念

能够且只能在两端进行插入和删除的数据结构,双端队列的典型应用是单调队列,单调队列可以优化问题的求解

双端队列的实现方式

c++ STL - deque

  • dq[i]:返回队列中下标为i的元素
  • dq.front():返回队头
  • dq.back():返回队尾
  • dq.pop_back():删除队尾,不返回值
  • dq.pop_front():删除队头,不返回值
  • dq.push_back(e):在队尾添加一个元素e
  • dq.push_front(e):在队头添加一个元素e

单调队列和滑动窗口

题目链接:P1886 滑动窗口 /【模板】单调队列 - 洛谷题库

解题思路:
思路一:暴力解
从头到尾扫描,总共经过n次,每次遍历k个元素,所以时间复杂度为O(n * k),这样会超时。

思路二:单调队列求解
时间复杂度为O(n)

本题中单调队列的特征

  • 队头的元素始终是单调队列中最小的。根据需要输出队头元素,但是不一定要弹出
  • 元素只能从队尾进入队列,从队头、队尾都可以弹出(限定插入的双端队列)
  • 序列中的每一个元素都必须进入队列。例如:x进入队尾时,和原来队尾元素y进行比较,如果x <= y就从队尾弹出y;一直弹出队尾所有比x大的元素,最后x进入队尾。这个入队操作保证队头元素是队列中最小的
  • 队伍的长度不会超过k,如果大于k,队头元素出队
    上述的操作就会保证这个队列始终满足单调性:从队头到队尾,从小到大,且队伍始终只有k个元素,每次输出队头和队尾元素,就是当前滑动窗口的最小值和最大值。每个元素只会入队一次,出队一次,总共有n个元素,所以时间复杂度为O(n)
#include<iostream>
#include<deque>
using namespace std;
const int N = 1e6 + 5;
int a[N];
deque<int>q;// 定义了一个双端队列,存储元素的下标
int main(){
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i ++){ //从下标为1的地方开始读入的原因
        cin >> a[i];
    }

    //求最小值
    for(int i = 1; i <= n; i ++){
       // x进入队尾时,和原来队尾元素y进行比较,如果`x <= y`就从队尾弹出y;一直弹出队尾所有比x大的元素,最后x进入队尾。
        while(!q.empty() && a[q.back()] > a[i]) {
            q.pop_back();
        }
        q.push_back(i);

        //确保窗口始终为m
        if(i >= m){
            while(!q.empty() && q.front() <= i - m){
                q.pop_front(); // 删头
            }
           cout << a[q.front()] <<" ";//输出队头元素,就是输出当前滑动窗口的最小值
        }
    }
    cout << endl;

    //求最大值,保证队头的最大值
    while(!q.empty()){
        q.pop_front();//清空
    }

    for(int i = 1; i <= n; i ++){
        while(!q.empty() && a[q.back()] < a[i]){
            q.pop_back();//去尾
        }
        q.push_back(i);
        if(i >= m){
            while(!q.empty() && q.front() <= i - m){
                q.pop_front();//删头
            }
            cout << a[q.front()] << " ";
        }
    }
    cout << endl;
    return 0;
}

单调队列和最大子序和问题

子序和:对于给定长度为n的整数序列A,子序和就是A中非空的一段连续的元素之和。例如(6, -1, 5, 4, -7)前两个元素的子序和为6 + (-1) = 5

最大子序和问题分类以及对应的解题方法

  • 不限制子序列的长度: 在所有可能的子序列中找到一个子序列,该子序列和最大。
    • 方法一:贪心法
    • 方法二:动态规划法
  • 限制子序列的长度:给定一个限制长度m,找出一段长度不超过m的子序和最大的连续子序列。
    • 方法一:单调队列

优先队列

优先队列:每次让优先级最高的元素出队列,是利用堆实现的,一般使用STL完成。

后进先出

STL stack

常见操作:

  • stack<Type>s:定义栈
  • s.push(item):将item放到栈顶
  • s.top():返回栈顶元素,但不会删除
  • s.pop():删除栈顶元素,但不会返回
  • s.size():返回栈中元素的个数
  • s.empty():检查栈是否为空,为空返回true,否则返回false

题目链接:Text Reverse - 杭电题库

#include<iostream>
#include<stack>
using namespace std;
int main(){
    int t;
    cin >> t;//次数
    getchar();//读取换行符(输入t之后的那个换行符号),防止后序读入字符受到影响
    while(t --){
        stack<char>s;
        while(true){
            char ch = getchar();//读入一个字符
            if(ch == ' ' || ch == '\n' || ch == EOF){// 当遇到空格、换行符或文件结束符时
                while(!s.empty()){
                    printf("%c",s.top());
                    s.pop();
                }
                if(ch == '\n' || ch == EOF){
                    break;
                }
            }else{
                s.push(ch); // 入栈
            }
        }
        printf("\n");
    }
    return 0;
}

手写栈

题目链接:Text Reverse - 杭电题库

确实手写栈会节省空间,但是TLE
在这里插入图片描述

#include<iostream>
using namespace std;
const int N = 1e5 + 100;
struct mystack{
    char a[N];
    int t = 0;
    void push(char x){ //入栈
        a[++ t] = x;
    }
    char top(){ //取栈顶元素
        return a[t];
    }
    char pop(){//出栈
        t --;
    }
    int empty(){ //判空
        return t == 0 ? 1 : 0;
    }
}st;
int main(){
    int n;
    cin >> n;
    getchar();
    while(n -- ){
        while (true){
            char ch = getchar();
            if(ch == ' ' || ch == '\n' || ch == EOF){
                while(!st.empty()){
                    printf("%c",st.top());
                    st.pop();
                }
                if(ch == '\n' || ch == EOF){
                    break;
                }
                cout << " ";
            }else{
                st.push(ch);
            }
        }
        cout << endl;
    }
    return 0;
}

单调栈

结构上,任然是一个普通的栈,它是栈的一种使用方式。
单调栈根据元素之间的关系,可以分为单调递增栈和单调递减栈,单调栈可以用来处理比较问题。
保持单调性的操作:例如单调递减栈,从栈底到栈定,单调递减(大到小),此时,元素入栈,如果元素比第一个元素还要小,则入栈,否则将栈顶元素弹出,直到这个数能入栈为止。

题目链接:P2947 [USACO09MAR] Look Up S - 洛谷题库

解题思路:
例如:6头奶牛依次为3 2 6 1 1 2,那么输出结果为3 3 0 6 6 0(仰望对象的下标,下标从1开始,没有仰望对象的话就是0)。如果常规思路,每一个位置,都去查找其后面的仰望对象,那么时间复杂度是O(n!),这样做会超时。可以用单调栈来优化。
单调栈的做法:从后向前遍历奶牛,用栈保存从低到高的奶牛,栈顶的最矮,栈底的最高(单调栈)。

  • 从后向前遍历奶牛,和栈顶的奶牛进行比较,如果栈顶的元素没有奶牛i高,则出栈,直到栈顶的奶牛比奶牛i高,这就是奶牛i的仰望对象
  • 将奶牛i放进栈顶,此时栈中的奶牛依然保持从低到高。
  • 由于每头奶牛只进出栈一次,所以时间复杂度为O(n)
#include<iostream>
#include<stack>
using namespace std;
const int N = 1e5 + 5;
int h[N], ans[N];
int main(){
    //完成读入
    int n;
    cin >> n;
    for(int i = 1; i <= n; i ++){
        cin >> h[i];
    }

    stack<int>st;//存下标
    //从后向前遍历奶牛
    for(int i = n; i >= 1; i --){
        //循环后,保证奶牛i小于栈顶元素
        while (!st.empty() && h[st.top()] <= h[i]){
            st.pop();
        }
        //如果栈为空,没有仰望对象,存0
        if(st.empty()){
            ans[i] = 0;
        } else {
            ans[i] = st.top();
        }
        st.push(i);
    }

    //输出结果
    for(int i = 1; i <= n; i ++){
        cout << ans[i] << endl;
    }
    return 0;
}

二叉树和哈夫曼树

树是非线性结构,能够描述数据的层次关系。
二叉树是常见的树形结构,相关的算法比较多,一般的树都转换为二叉树来解决问题
哈夫曼树是二叉树的一个应用

二叉树的概念

二叉树的遍历

哈夫曼树和哈夫曼编码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值