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;
}
posted @ 2021-10-26 13:25  IdanSuce  阅读(116)  评论(0编辑  收藏  举报