C++ 线性表
双端队列deque
两边都可以插入和取出的容器。
想象成一根长长的透明管子,两边都有开口
你可以从两边任意一个口塞一个对象进去
(然后我就有了很多对象)
但是由于某些物理学圣剑的原因,你想要拿出对象的时候只能先把挤在外面的拿出来,然后才能拿出里面的。
(但是我可以通过迭代器iterator知道里面都有谁)
对应的操作就是
std::deque<int> q;
q.push_front(a);
q.push_back(a);
q.pop_front();
q.pop_back();
q.size();
q.empty();
q.front();
q.back();
q.rbegin();
q.rend();
栈stack
一边封死了的双端队列,你只能从一端操作你的对象,比如说压入栈,弹出栈。
对应的操作就是
std::stack<int> s;
s.push(a);
s.pop();
s.top();
s.size();
s.empty();
队列queue
两边各封了一半的双端队列。一边只允许出队,称为队头。一边只允许入队,称为队尾。
std::queue<int> q;
q.push(a);
q.pop();
q.front();
q.back();
q.size();
q.empty()
最常用于广度优先搜索算法中。
链表
我不知道为什么会这么难
首先定义一个结构体
struct Node;
注意,这里的结构体遵循的是c++语法,对于c的小伙伴们……自行脑补
我们可以往里面塞什么?
int/float/string/template
但是就是不能塞Node
要不然重复定义……emmm直接炸掉
但是我们知道,指针,是可以往里面塞的。
指针是什么?就是地址。我只需要知道你的地址,我就可以找到你,然后访问你,不必要知道你的全部信息。
比如说,现在排队排了一列人,我要从队头遍历到队尾。首先我保存队头的地址,然后队列里面每个人都保存下一个人的地址。这样我每次都能找到队头,然后队头知道第二的地址,我就能找到第二。第二知道第三,我就能知道第三……
我并不需要知道队头家里都住了些什么人,有些什么东西,是小姐姐还是小哥哥……反正我有地址这些东西遍历的时候都能看到。
写成代码就是
struct Node
{
Node *nxt; // 储存下一个人的地址(为空就是没有)
int dat; // 用来储存数据
Node(int k=0) { nxt=NULL; dat=k; } // 初始化dat为k,下一个人是没有人
};
Node *head,*ed; // 定义队头,队尾(为了方便)
void init()
{
/*
用来初始化链表(你喜欢的话写在主函数里也可以)
@param: void
@return: void
*/
head=new Node; // 初始化队头
ed=head; // 队尾就是队头,因为一个元素也没有
return;
}
void insert(int k)
{
/*
用来向链表中插入一个元素k
@param: int k 要插入的元素
@return: void
*/
ed->dat=k; // 等价于(*ed).dat=k;
ed->nxt=new Node; // 那这个就不是结尾了(结尾表示结束,不存任何数据),所以新建一个结尾
ed=ed->nxt; // 更新结尾
return;
}
void del(int k)
{
/*
删除第一个为k的元素
@param: int k 要删除的元素
@return: void
*/
Node *tmp=head; // 定义临时变量用来遍历链表
while(tmp!=ed) // 遍历到末尾,不管有没有找到都要退出了。
{
if(tmp->nxt!=ed&&tmp->nxt->dat==k) // 下一个元素就是k了
{
// 删除掉这个元素,就是我不要这个元素了,我直接忽略他。(画个图理解一下)
// 但是如果下一个元素是末尾的话。。。删了会RE的
tmp->nxt=tmp->nxt->nxt;
break; // 找到了,删除了,就可以走了
}
tmp=tmp->nxt; // 然后遍历下一个元素。
}
return;
}
void del_all(int k)
{
/*
删除全部的k
@param: int k 要删除的元素
@return: void
*/
Node *tmp=head; // 定义临时变量用来遍历链表
while(tmp!=ed) // 遍历到末尾,不管有没有找到都要退出了。
{
while(tmp->nxt!=ed&&tmp->nxt->dat==k) // 下一个元素就是k了
{
// 删除掉这个元素,就是我不要这个元素了,我直接忽略他。(画个图理解一下)
// 有可能删除之后链接的下一个元素还是k,所以要用while
tmp->nxt=tmp->nxt->nxt;
}
tmp=tmp->nxt; // 然后遍历下一个元素。
}
return;
}
void insert(Node* p,int k)
{
/*
在节点p后面插入一个元素为k的节点
@param: Node* p 指向那个节点的指针
int k 要插入的元素k
@return: void
*/
Node *now=new Node(k); // 先新建一个对象节点,值为k
now->nxt=p->nxt; // 要插入到p后面,所以now的下一个应该就是原来p的下一个
p->nxt=now; // 然后现在p的下一个就应该是now了
return;
}
单向链表
就是只能单向遍历的链表,保存队头,然后一路nxt访问,就像上面
双向链表
多了一个pre指针指向前一个元素
struce Node
{
Node *pre,*nxt;
int dat;
Node(int k=0)
{
pre=nxt=0;
dat=k;
}
}
void init()
{
/*
用来初始化链表(你喜欢的话写在主函数里也可以)
@param: void
@return: void
*/
head=new Node; // 初始化队头
ed=head; // 队尾就是队头,因为一个元素也没有
return;
}
void insert(int k)
{
/*
用来向链表中插入一个元素k
@param: int k 要插入的元素
@return: void
*/
ed->dat=k; // 等价于(*ed).dat=k;
ed->nxt=new Node; // 那这个就不是结尾了(结尾表示结束,不存任何数据),所以新建一个结尾
ed->nxt->pre=ed; // 新结尾的前一个节点应该就是原来的结尾
ed=ed->nxt; // 更新结尾
return;
}
void del(int k)
{
/*
删除第一个为k的元素
@param: int k 要删除的元素
@return: void
*/
Node *tmp=head; // 定义临时变量用来遍历链表
while(tmp!=ed) // 遍历到末尾,不管有没有找到都要退出了。
{
// 因为有了上一个节点的指针,我们删除起来也方便了,可以直接判断当前节点是否为要删除的节点
// 具体。。。看下去吧
if(tmp->dat==k) // 这个元素就是要删除的元素
{
// 删除掉这个元素,就是我不要这个元素了,我直接忽略他。(画个图理解一下)
// 删除之后还要保证链表的连续性,所以要把前一个元素和后一个元素连接起来
if(tmp->pre) tmp->pre->nxt=tmp->nxt; // 如果这个节点刚好是head,即开头就是k,那么这个节点是没有前驱节点的,也就是pre指针为空,那么。。。不用管它了。
// head的标志就是pre指针为空。pre非空就肯定不是head。那么我们就要告诉前一个节点:
// 我走了,你的下一个人不是我,是原来排在我后面的人。你要~~空虚寂寞冷~~的话找他去好了
// (莫名泪目(T^T))
tmp->nxt->pre=tmp->pre; // 但是后继没有这个担忧,因为后继最后有ed 垫着,你不可能遍历到ed,所以很安全直接处理
// 我就告诉后面的人:我走了,你要的话直接找前面的好了,别找我。
break; // 找到了,删除了,就可以走了
}
tmp=tmp->nxt; // 然后遍历下一个元素。
}
return;
}
void del_all(int k)
{
/*
删除第一个为k的元素
@param: int k 要删除的元素
@return: void
*/
Node *tmp=head; // 定义临时变量用来遍历链表
while(tmp!=ed) // 遍历到末尾,不管有没有找到都要退出了。
{
// 因为有了上一个节点的指针,我们删除起来也方便了,可以直接判断当前节点是否为要删除的节点
// 具体。。。看下去吧
if(tmp->dat==k) // 这个元素就是要删除的元素
{
// 删除掉这个元素,就是我不要这个元素了,我直接忽略他。(画个图理解一下)
// 删除之后还要保证链表的连续性,所以要把前一个元素和后一个元素连接起来
if(tmp->pre) tmp->pre->nxt=tmp->nxt; // 如果这个节点刚好是head,即开头就是k,那么这个节点是没有前驱节点的,也就是pre指针为空,那么。。。不用管它了。
// head的标志就是pre指针为空。pre非空就肯定不是head。那么我们就要告诉前一个节点:
// 我走了,你的下一个人不是我,是原来排在我后面的人。你要~~空虚寂寞冷~~的话找他去好了
// (莫名泪目(T^T))
tmp->nxt->pre=tmp->pre; // 但是后继没有这个担忧,因为后继最后有ed 垫着,你不可能遍历到ed,所以很安全直接处理
// 我就告诉后面的人:我走了,你要的话直接找前面的好了,别找我。
// 找到了删除了但是不能走,因为要把全部应该走的都踢走(嘤嘤嘤)
}
tmp=tmp->nxt; // 然后遍历下一个元素。
}
return;
}
void insert(Node* p,int k)
{
/*
在节点p后面插入一个元素为k的节点
@param: Node* p 指向那个节点的指针
int k 要插入的元素k
@return: void
*/
Node *now=new Node(k); // 先新建一个对象节点,值为k
now->nxt=p->nxt; // 要插入到p后面,所以now的下一个应该就是原来p的下一个
now->pre=p; // 然后now的前一个就应该是p了
p->nxt->pre=now;
// p在告诉下一个人:你前面插了个人,你要找我的话要先找到他
p->nxt=now; // 然后现在p的下一个就应该是now了
return;
}
循环链表
链表尾部不指向ed反而指回head,这样就可以实现重复循环遍历链表,无论你从链表哪里开始都可以遍历完整个链表。(实现思想)
具体代码……自己写去
正所谓即得易见平凡,仿照上例显然,留作习题答案略,读者自证不难
循环链表有什么用呢?比如说下面这个约瑟夫问题
我们可以通过循环链表来避免取模运算,(虽然一加一模比我慢慢走更快)然后我们要进行循环链表的训练,但是我知道你们肯定懒得自己写所以代码如下:
#include <iostream>
struct Node
{
Node *nxt;
int num;
Node (int k=0) {nxt=0; num=k;}
}*hd,*ed;
void init()
{
hd=new Node;
ed=hd;
return;
}
void insert(int k)
{
ed->num=k;
ed=(ed->nxt=new Node);
return;
}
void del(Node *p)
{
/*
删除p的下一个节点
@param: Node *p 给定节点的指针
@return: void
*/
p->nxt=p->nxt->nxt; // 因为是循环链表,所以直接忽略下一个节点就可以了。
}
int n,m;
int main()
{
std::cin>>n>>m;
init();
for(int i=1;i<=n;++i) insert(i);
Node *p=hd;
while(p->nxt!=ed&&p!=ed) p=p->nxt; // 找到链表中最后一个元素
p->nxt=hd; // 让他的后继指向头指针
// 这样就构成了一个循环链表,可以反复访问。
p=hd; // 从头开始出圈
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m-2;++j) p=p->nxt;// 报到m的人,也就是从现在开始数下m-1个人,要删除这个人,我们就要找到他的前驱,所以是跳m-2次
std::cout<<p->nxt->num<<' ';
del(p);
p=p->nxt; // 然后从下一个人开始报1
}
std::cout<<std::endl;
return 0;
}