算法基础第二章

算法基础第二章

  • 第二章:数据结构
    • 1、链表
      • 1.1、单链表(写邻接表:存储图和树)
      • 1.2、双链表(优化某些问题)
    • 2、栈与队列
    • 2.1、栈
      • 2.1.1、数组模拟栈
      • 2.1.2、单调栈
    • 2.2、队列
      • 2.2.1、数组模拟队列
      • 2.2.2、滑动窗口(单调队列的使用)
    • 3、KMP(字符串匹配)
    • 4、Trie树(高效地存储和查找字符串集合的数据结构)
    • 5、并查集
    • 6、堆
    • 7、Hash表
      • 7.1、字符串前缀哈希(KMP的劲敌)
    • 8、STL常用容器(用的时候查一下就行了)
      • 8.1、vector
      • 8.3、pair
      • 8.2、string
      • 8.3、queue(队尾进队头出,固定方向)
      • 8.4、priority_queue(优先队列就是堆,默认是大根堆)
      • 8.5、stack
      • 8.6 deque(双端队列)
      • 8.7 set、map、multiset、multimap(基于平衡二叉树(红黑树),动态维护有序序列)
      • 8.8、unordered_set、unordered_map、unordered_multiset、unordered_multimap(哈希表)
      • 8.9、bitset

第二章:数据结构

1、链表

  • 笔试中遇到需要链表写法解题的话尽量用数组替代,而不是用new创建节点,new非常的费时间

1.1、单链表(写邻接表:存储图和树)

  • 解析:如下四句输入,在0前面插入1的时候,0不是链表中的节点,插入链表尾,ne[0]是头指针,里面存着1,意思是头指针指向1节点,整个过程如下图所示。ne保存的是指向下一个元素的位置值,e保存的是自己本身的值。
    算法基础第二章_第1张图片算法基础第二章_第2张图片
  • 题目链接:数组模拟链表
  • 代码:这个代码使用了数组,并未使用stl,但并没有全a,后面输入数据过大不给调试,望路过的大佬能够补充
#include 
#include
#include
#include

using namespace std;

const int N = 10010;
typedef pair<long long,int>PII;

int n;
PII tmp[N];//保存元素的位置和该元素的值
long long e[N];//存储元素值得数组
int ne[N];//存储元素指向的下一个位置
int cur = 1;//当前链表的位置

void init()
{
    memset(tmp,0x7fffffff,sizeof tmp);//初始化为最大值
}

int findx(int cur,int x)
{
    while(cur>=0)
    {
        if(tmp[cur].first==x)
        {
            return tmp[cur].second;//返回x元素在数组中的位置
        }
        cur--;
    }
    return -1;
}

int findy(int cur,int x)
{
    while(cur>=0)
    {
        if(e[ne[cur]] == x)
        {
            return cur;//返回指向x元素的元素的位置
        }
        cur--;
    }
    return -1;
}

void insert(int x,int y)
{
    auto t = findx(cur,x);//这个是寻找x自己所在的位置
    auto u = findy(cur,x);//这个是寻找指向x的元素所在的位置
    if(t != -1 && u != -1)//链表里面有这个节点,则在x前插入y,且排除之前插入后又被删除的元素
    {
        e[cur] = y;
        ne[cur] = t;
        tmp[cur].first = y,tmp[cur].second = cur;
        ne[u] = cur,cur++;
    }
    else {//链表里面没这个节点,则在链表末尾插入这个元素,还要分是不是空链表
        if(ne[0])//非空链表
        {
            e[cur] = y;
            tmp[cur].first = y,tmp[cur].second = cur;
            ne[cur] = 0;
            ne[cur-1] = cur,cur++;
        }
        else {//空链表,需要处理头指针
            e[cur] = y;
            ne[0] = cur;//头指针指向当前位置
            tmp[cur].first = y,tmp[cur].second = cur;
            ne[cur] = 0;
            cur++;
        }
    }
}

void del(int x)
{
    auto t = findy(cur,x);
    auto u = findx(cur,x);
    if(t != -1)
    {
        ne[t] = ne[u];//将指向需要被删除的元素的指针指向需要被删除的元素指向的元素
    }
}

int main() {
    init();
    scanf("%d",&n);
    char str[7];
    int x,y,z;
    while(n--)
    {
        scanf("%s",str);
        if(!strcmp(str,"insert"))
        {
            scanf("%d%d",&x,&y);
            insert(x, y);
        }
        else {
            scanf("%d",&z);
            del(z);
        }
    }
    if(ne[0])//链表为非空
    {
        for(int i=ne[0];i;i=ne[i])
        {
            printf("%lld ",e[i]); 
        }
    }
    else {
        printf("NULL");
    }
    return 0;
}
// 64 位输出请用 printf("%lld")

1.2、双链表(优化某些问题)

  • 解析:
  • 题目链接:
  • 代码

2、栈与队列

2.1、栈

2.1.1、数组模拟栈

  • 解析:用一个top指针来进行栈的弹出与压入,压栈top加,出栈top减
    算法基础第二章_第3张图片
  • 题目链接:数组模拟栈
  • 代码
#include 
#include
#include
#include

using namespace std;

const int N = 100010;
int s[N];//栈数组
int n;
int top;//top指针

int main() {
    char opt[5];//输入的操作字符串
    int num;//如果是push则还有输入操作数
    scanf("%d",&n);
    for(int i = 0; i < n; i++)
    {
        scanf("%s",opt);
        if(!strcmp(opt,"push"))//比较成功strcmp返回0
        {
            scanf("%d",&num);
            s[++top] = num;//数组的0的位置用来标识栈为空,0位置不存元素,这是压栈
        }
        else if(!strcmp(opt,"pop"))
        {
            if(top)
            {
                printf("%d\n",s[top--]);//top指针减1相当于出栈
            }
            else {
                printf("error\n");
            }
        }
        else {
            if(top)
            {
                printf("%d\n",s[top]);
            }
            else {
                printf("error\n");
            }
        }
    }
    return 0;
}

2.1.2、单调栈

  • 解析:在一个可能包含重复数字的序列中,找到每个数左边最近比它小的数,栈的压入和弹出如下图所示
    算法基础第二章_第4张图片
  • 题目链接:单调栈的应用
  • 代码
#include 
class Solution {
public:
    vector<vector<int>> foundMonotoneStack(vector<int>& nums) {
        // write code here.
        const int N = 100010;
        int n = nums.size();
        int l[N],r[N],tl=0,tr=0;
        vector<vector<int>>arry(n,vector<int>(2));
        for(int i = 0; i < n; i++)
        {
            while(tl && nums[l[tl]] >= nums[i])
                tl--;
            if(tl)
                arry[i][0] = l[tl];
            else
                arry[i][0] = -1;
            l[++tl] = i;
        }
        for(int i = n-1; i >= 0; i--)
        {
            while(tr && nums[r[tr]] >= nums[i])
                tr--;
            if(tr)
                arry[i][1] = r[tr];
            else
                arry[i][1] = -1;
            r[++tr] = i;
        }
        return arry;
    }
};

2.2、队列

2.2.1、数组模拟队列

  • 解析:通过一个头指针和尾指针来进行队列的压入与弹出,压入的时候队尾指针加,弹出的时候队头的指针加,front的位置是h+1
    算法基础第二章_第5张图片
  • 题目链接:数组模拟队列
  • 代码
#include 
#include 

using namespace std;

const int N = 100010;
int q[N];//队列数组
int n;
int hh,tt;//队头队尾指针

int main() {
    scanf("%d",&n);
    char opt[5];
    int num;
    for(int i = 0; i < n; i++)
    {
        scanf("%s",opt);
        if(!strcmp(opt,"push"))
        {
            scanf("%d",&num);
            q[++tt] = num;//从队尾插入,所以队尾指针先加1在放入元素
        }
        else if(!strcmp(opt,"pop"))
        {  
            if(hh == tt)//队头指针和队尾指针相等的时候表示队列是空的
            {
                printf("error\n");
            }
            else {
                printf("%d\n",q[++hh]);//从队头弹出元素,队头指针先加1再弹出
            }
        }
        else {
            if(hh != tt)
            {
                printf("%d\n",q[hh+1]);//非空的时候,front的元素为头指针加1的位置
            }
            else {
                printf("error\n");
            }
        }
    }
}

2.2.2、滑动窗口(单调队列的使用)

  • 解析:这个问题一般就三步:1、什么时候队头需要前进;2、队头的最大值或者最小值是否要更新;3、什么时候输出或者保存队头的最大或最小值
    算法基础第二章_第6张图片
  • 题目链接:滑动窗口
  • 代码
class Solution {
public:
    vector<int> maxInWindows(const vector<int>& num, unsigned int size) {
        const int N = 10010;
        int n = num.size();
        int q[N],front=0,tail=-1;
        vector<int>ret;
        for (int i = 0; i < n; i++) {
            int a = i - size + 1;//单独写这一句,直接写在if里面会出错,i是int,size是unsigned int,当是负数的时候会出问题
            //超出了滑动窗口的个数front就要加加
            if (front <= tail && a > q[front]) front++;
            //要输出最大值,则队头维护的是窗口里面的最大值
            while (front <= tail && num[i] >= num[q[tail]]) tail--;
            q[++tail] = i;
            if (i >= size - 1) ret.push_back(num[q[front]]);
        }
        return ret;
    }
};

3、KMP(字符串匹配)

  • 解析:解析,较抽象
  • 题目链接:KMP算法
  • 代码:
#include 
#include

using namespace std;

const int N = 500010;

int main() {
    
    string str1,str2;
    cin >> str1 >> str2;
    char S[N],P[N];
    bool flag = false;
    for(int i = 0; i < str1.length();i++)
    {
        S[i+1] = str1[i];
    }    
    for(int i = 0; i < str2.length();i++)
    {
        P[i+1] = str2[i];
    }
    int ne[N];
    for(int i = 2,j = 0; i <= str2.length(); i++)
    {
        while(j && P[i] != P[j+1]) j = ne[j];
        if(P[i] == P[j+1]) j++;
        ne[i] = j;
    }

    for(int i = 1,j = 0;i <= str1.length(); i++)
    {
        while(j && S[i] != P[j+1]) j = ne[j];
        if(S[i] == P[j+1]) j++;
        if(j == str2.length())
        {
            flag = true;
            cout << i-str2.length() << " ";
            j = ne[j];
        }
    }
    if(flag)
        return 0;
    else
        cout << -1;
}

4、Trie树(高效地存储和查找字符串集合的数据结构)

  • 解析:trie树的存储方式如下所示,从根节点开始,依次找子节点,没有就新创建一个,并在每个单词的末尾打上一个标记,表示这是一个完整的单词
    算法基础第二章_第7张图片
  • 题目链接:字典树的实现
  • 代码

5、并查集

  • 解析:每个集合的根节点就是这个集合的编号,初始每个节点各自为一个集合,合并1,2这两个集合只需要让2作为1的父节点,节点1里面存的就是2这个父节点的值
    算法基础第二章_第8张图片
  • 题目链接:并查集的实现
  • 代码:一下几次提交结果是使用cin和cout以及scanf和printf的结果,大数据量的时候使用cin读入和cout读出特别费时间,所以最好使用scanf和printf以节省时间。
    算法基础第二章_第9张图片
#include 
using namespace std;

const int N = 1000010;
int n,m;
int p[N];//存储父节点的树,用数组替代

/* find这部分的代码是最重要的 */
int find(int x)
{
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int main() {
    scanf("%d%d",&n,&m);
    //初始的时候每个节点独此为一个集合,所以存储的父节点就是自己
    for(int i = 1; i <= n; i++)
    {
        p[i] = i;
    }
    while(m--)
    {
        int opt,a,b;
        scanf("%d%d%d",&opt,&a,&b);
        if(opt == 1)
        {
            if(find(a) == find(b)) printf("Yes\n");
            else printf("No\n");
        }
        else if(opt == 2)
        {
        	//合并两个子集就是将b作为a的父节点
            p[find(a)] = find(b);
        }
    }
}

6、堆

  • 解析:整个的原理如下所示,堆本质是一颗完全二叉树,但可以通过数组来实现存储。不管是插入还是删除操作,做完之后都要针对该位置重新更新一下排序
    算法基础第二章_第10张图片
  • 题目链接:堆模板
  • 代码
#include 
#include
#include
#include

using namespace std;

const int N = 100010;
int arry[N],len;//数组存储,len数组里面的个数

void up(int x)
{
	//向上排,当前元素比父节点的元素大,则当前元素与父节点的元素交换
    while(x/2 && arry[x] > arry[x/2])
    {
        swap(arry[x],arry[x/2]);
        x >>= 1;//当前元素变成父节点
    }
}

void down(int x)
{
    int t = x;
    if(x*2 <= len && arry[x*2] > arry[t]) t = x * 2;
    if(x*2+1 <= len && arry[x*2+1] > arry[t]) t = x * 2 + 1;
    if(t != x)
    {
        swap(arry[t],arry[x]);
        down(t);
    }
}

int main() {
    int n;
    scanf("%d",&n);
    char opt[5];
    int num;
    for(int i = 0; i < n; i++)
    {
        scanf("%s",opt);
        if(!strcmp(opt,"push"))
        {   
            scanf("%d",&num);
            arry[++len] = num;
            up(len);
        }
        else if(!strcmp(opt,"pop"))
        {
            if(len)
            {
                printf("%d\n",arry[1]);
                arry[1] = arry[len--];
                down(1);
            }
            /* 最后一个元素覆盖根节点元素,再down一遍 */
            else {
                printf("%s\n","empty");
            }
        }
        else if(!strcmp(opt,"top")){
            /* top操作 */
            if(len)
                printf("%d\n",arry[1]);//直接输出顶元素
            else 
                printf("%s\n","empty");
        }
    }
    return 0;
}

7、Hash表

  • 作用:将一些较大的值域/复杂的数据映射到一个较小的范围(-109~109)->(0,10^5)
    • x mod 10^5 ----> (0,10^5)
    • 可能会有冲突:通过拉链法或者开放寻址法处理冲突
  • 哈希表
    • 存储结构
      • 开放寻址法
      • 拉链法
    • 字符串哈希方式
  • 解析:

7.1、字符串前缀哈希(KMP的劲敌)

  • 解析:
  • 题目链接:
  • 代码

8、STL常用容器(用的时候查一下就行了)

8.1、vector

  • size和empty所有的都有这个,但clear有的没有
    • size():返回元素个数
    • empty():是否为空
  • clear():清空
  • front()/back()
  • push_back()/pop_back()
  • begin()/end()
  • []:随机选址
  • 支持比较运算(按字典序)

8.3、pair

  • first、second、支持比较运算(以first为第一关键字,second为第二关键字)
  • p = make_pair(10,“lxh”);

8.2、string

  • size()/length()
  • empty()、clear()
  • substr(a,b):返回a开始的长度为b的字串
  • c_str():返回string 的首地址
#include
#include
#include
#include

using namespace std;

int main()
{
    string s = "lxh";
    cout << s.substr(1,2) << endl;
    printf("%s",s.c_str());
    return 0;
}

8.3、queue(队尾进队头出,固定方向)

  • **size()、empty()、没有clear() **
  • push():向队尾插入一个元素
  • front():返回队头元素
  • back():返回队尾元素
  • pop():弹出队头元素

8.4、priority_queue(优先队列就是堆,默认是大根堆)

  • push():插入一个元素
  • top():返回堆顶元素
  • pop():弹出堆顶元素
  • 定义小根堆
priority_queue<int,vector<int>,greater<int>>hp;//小根堆

8.5、stack

  • 没有clear()

8.6 deque(双端队列)

8.7 set、map、multiset、multimap(基于平衡二叉树(红黑树),动态维护有序序列)

8.8、unordered_set、unordered_map、unordered_multiset、unordered_multimap(哈希表)

8.9、bitset

你可能感兴趣的:(算法编程,算法,链表,数据结构)