环形数据结构用法归纳与总结

这里环形数据结构主要包括:环形链表、环形队列等。

一、环形链表

1. 如何判断一个链表是否有环

1.1 快慢指针

判断单链表是否有环

判断链表是否存在环的办法一般是设置两个指针(fast,slow),初始值都指向头指针,slow每次前进一步,fast每次前进两步,如果链表存在环,则fast必定先进入环,而slow后进入环,两个指针必定相遇。(当fast先行从头到尾部为NULL,则为无环链表)

具体看示例代码(单链表)

struct node{
    char val;
    node *next;
}

bool check_loop(const node* head){
   if(head == NULL)
       return false;//无环
   
   node* pSlow  = head;
   node* pFast = head->next;
   while(pFast != NULL && pFast->next != NULL)
   {
       pSlow = pSlow->next;
       pFast = pFast->next->next;
       if (pSlow == pFast)
       {
           return true;//有环
       }
   }
   return false;//无环      
}

判断双链表是否有环

其实跟单链表判断环的方法相似,只是当判断next指针不会出现环时,要从尾节点按照之前的方法向头结点扫描,判断pre指针是否可能出现环,如图环2。当然如果在第一步判断链表有next环后是无法进行第二步判断的,因为你永远找不到尾节点。

具体看示例代码(双链表)

struct node{
    char val;
	node *prev;
    node *next;
}

bool check_loop(const node* head){
   if(head == NULL)
       return false;//无环
   
   node* pSlow  = head;
   node* pFast = head;
   
   //与单链表类似,使用快慢指针先单向遍历到结尾,如果相遇证明起码单向有环
   while(pFast != NULL && pFast->next != NULL)
   {
       pSlow = pSlow->next;
       pFast = pFast->next->next;
       if (pSlow == pFast)
       {
           return true;//有环
       }
   }
   
   //一直遍历直到找到尾指针为止
   while(pSlow.next != null){
		pSlow = pSlow.next;
   }
   
   //如果next单向没环,从尾节点回溯遍历,看pre是否存在环
   	pFast = pSlow;
	while(pFast != null && pFast.prev != null){
		pSlow = pSlow.prev;
		pFast = pFast.prev.prev;
		if(pSlow == pFast){
			return true;
		}
	}
   
   return false;//无环  
}

1.2 穷举遍历

首先从头节点开始,依次遍历单链表的每一个节点。每遍历到一个新节点,就从头节点重新遍历新节点之前的所有节点,用新节点ID和此节点之前所有节点ID依次作比较。如果发现新节点之前的所有节点当中存在相同节点ID,则说明该节点被遍历过两次,链表有环;如果之前的所有节点当中不存在相同的节点,就继续遍历下一个新节点,继续重复刚才的操作。

例如这样的链表:A->B->C->D->B->C->D, 当遍历到节点D的时候,我们需要比较的是之前的节点A、B、C,不存在相同节点。这时候要遍历的下一个新节点是B,B之前的节点A、B、C、D中恰好也存在B,因此B出现了两次,判断出链表有环。

假设从链表头节点到入环点的距离是D,链表的环长是S。那么算法的时间复杂度是0+1+2+3+….+(D+S-1) = (D+S-1)*(D+S)/2 , 可以简单地理解成 O(N*N)。而此算法没有创建额外存储空间,空间复杂度可以简单地理解成为O(1)。

1.3 哈希表缓存

首先创建一个以节点ID为键的HashSet集合,用来存储曾经遍历过的节点。然后同样是从头节点开始,依次遍历单链表的每一个节点。每遍历到一个新节点,就用新节点和HashSet集合当中存储的节点作比较,如果发现HashSet当中存在相同节点ID,则说明链表有环,如果HashSet当中不存在相同的节点ID,就把这个新节点ID存入HashSet,之后进入下一节点,继续重复刚才的操作。

这个方法在流程上和穷举遍历类似,本质的区别是使用了HashSet作为额外的缓存。

假设从链表头节点到入环点的距离是D,链表的环长是S。而每一次HashSet查找元素的时间复杂度是O(1), 所以总体的时间复杂度是1*(D+S)=D+S,可以简单理解为O(N)。而算法的空间复杂度还是D+S-1,可以简单地理解成O(N)。

2. 如何判断一个链表是否为空

pHead为指向表头结点的指针,分别写出带有头结点的单链表、单项循环链表和双向循环链表判空的条件

单链表 :    NULL==pHead->next
单向循环 :pHead==pHead->next
双向循环 :pHead==pHead->next&&pHead==pHead->pre

3. 如何找出有环链表的入环点?

当fast若与slow相遇时,slow肯定没有走遍历完链表(不是一整个环,有开头部分,如上图)或者恰好遍历一圈(未做验证,看我的表格例子,在1处相遇)。于是我们从链表头、相遇点分别设一个指针,每次各走一步,两个指针必定相遇,且相遇第一点为环入口点(慢指针走了n步,第一次相遇在c点,对慢指针来说n=s+p,也就是说如果慢指针从c点再走n步,又会到c点,那么顺时针的CB距离是n-p=s,但是我们不知道s是几,那么当快指针此时在A点一步一步走,当快慢指针相遇时,相遇点恰好是圆环七点B(AB=CB=s))。

参考:链表中环形的入口

代码如下:

struct node{
    char val;
	node *prev;
    node *next;
}Node;

Node *ploop_node(const node* head){
   if(head == NULL)
       return false;//无环
   
   
   node* pSlow  = head;
   node* pFast = head;
   bool isLoop = false;
   
   while(pFast != NULL && pFast->next != NULL)
   {
	   //使用快慢指针,慢指针每次向前一步,快指针每次两步
       pSlow = pSlow->next;
       pFast = pFast->next->next;
	   //两指针相遇则有环
       if (pSlow == pFast)
       {
		   isLoop = true;
           break;		   
       }
   }
   
	//一个指针从链表头开始,一个从相遇点开始,每次一步,再次相遇的点即是入口节点
	if(isLoop){
		pSlow  = head;
		while(pFast != NULL && pFast->next != NULL)
		{
			//两指针相遇的点即是入口节点
			if(pSlow == pFast)
			{
				return pSlow;
			}
			pSlow = pSlow->next;
			pFast = pFast->next;
		}
	}
 
   return  
}

4. 如何判断两个单链表是否相交?

参考:判断两个单链表是否相交

方法一、直接法

直接判断第一个链表的每个结点是否在第二个链表中,时间复杂度为O(len1*len2),耗时很大。

方法二、利用计数

如果两个链表相交,则两个链表就会有共同的结点;而结点地址又是结点唯一标识。因而判断两个链表中是否存在地址一致的节点,就可以知道是否相交了。可以对第一个链表的节点地址进行hash排序,建立hash表,然后针对第二个链表的每个节点的地址查询hash表,如果它在hash表中出现,则说明两个链表有共 同的结点。这个方法的时间复杂度为:O(max(len1+len2);但同时还得增加O(len1)的存储空间存储哈希表。这样减少了时间复杂度,增加了存储空间。

以链表节点地址为值,遍历第一个链表,使用Hash保存所有节点地址值,结束条件为到最后一个节点(无环)或Hash中该地址值已经存在(有环)。

再遍历第二个链表,判断节点地址值是否已经存在于上面创建的Hash表中。

这个方面可以解决题目中的所有情况,时间复杂度为O(m+n),m和n分别是两个链表中节点数量。由于节点地址指针就是一个整型,假设链表都是在堆中动态创建的,可以使用堆的起始地址作为偏移量,以地址减去这个偏移量作为Hash函数。

方法三、利用有环链表思路

对于两个没有环的链表相交于一节点,则在这个节点之后的所有结点都是两个链表所共有的。如果它们相交,则最后一个结点一定是共有的,则只需要判断最后一个结点是否相同即可。时间复杂度为O(len1+len2)。对于相交的第一个结点,则可求出两个链表的长度,然后用长的减去短的得到一个差值 K,然后让长的链表先遍历K个结点,然后两个链表再开始比较。

还可以这样:其中一个链表首尾相连,检测另外一个链表是否存在环,如果存在,则两个链表相交,而检测出来的依赖环入口即为相交的第一个。

二、环形队列

1. 循环队列是满、是空、是不为空的判断

参考:循环队列是满、空、非空判断

现有一个循环队列,其队头指针为 front,队尾指针为 rear,循环队列的总长度为 N,问怎么判断循环队列满了?(D)

A. front==rear
B. front==rear+1
C. front==rear%n
D. front==(rear+1)%N

  1. 当队列不为空时,front指向队列的第一个元素,rear指向队列最后一个元素的下一个位置。
  2. 当队列为空时,front=rear
  3. 队列满时:(rear+1)%maxsiz=front,少用一个存储空间,也就是数组的最后一个存数空间不用。

最大容量为N的循环队列,队尾指针是rear,队头是front,则队空的条件是(B)

A. (rear+1) MOD n=front
B. rear=front
C. rear+1=front
D. (rear-1) MOD n=front

循环队列的相关条件和公式:

1.队空条件:rear==front
2.队满条件:(rear+1) %QueueSize==front,其中QueueSize为循环队列的最大长度
3.计算队列长度:(rear-front+QueueSize)%QueueSize
4.入队:(rear+1)%QueueSize
5.出队:(front+1)%QueueSize

还可以参考:

判断循环队列是满还是空

循环队列中判断队满与队空

2. 环形队列与队列的不同之处

3. 环形队列代码Demo

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C++标准库中没有直接提供环形数据结构。然而,你可以使用其他数据结构来实现环形数据结构的功能。一种常见的方法是使用循环链表来模拟环形数据结构。循环链表是一种链表,其中最后一个节点指向第一个节点,形成一个闭环。通过这种方式,你可以在C++中实现环形队列、环形缓冲区等环形数据结构。 以下是一个使用循环链表实现环形队列的示例代码: ```cpp #include <iostream> template <typename T> class CircularQueue { private: struct Node { T data; Node* next; }; Node* front; Node* rear; public: CircularQueue() { front = nullptr; rear = nullptr; } void enqueue(T value) { Node* newNode = new Node; newNode->data = value; newNode->next = nullptr; if (front == nullptr) { front = newNode; } else { rear->next = newNode; } rear = newNode; rear->next = front; } T dequeue() { if (front == nullptr) { throw std::runtime_error("Queue is empty"); } T value = front->data; Node* temp = front; if (front == rear) { front = nullptr; rear = nullptr; } else { front = front->next; rear->next = front; } delete temp; return value; } bool isEmpty() { return front == nullptr; } }; int main() { CircularQueue<int> queue; queue.enqueue(1); queue.enqueue(2); queue.enqueue(3); while (!queue.isEmpty()) { std::cout << queue.dequeue() << " "; } return 0; } ``` 这段代码演示了如何使用循环链表实现一个环形队列。enqueue函数用于将元素添加到队列中,dequeue函数用于从队列中移除并返回元素。isEmpty函数用于检查队列是否为空。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值