这里环形数据结构主要包括:环形链表、环形队列等。
一、环形链表
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
- 当队列不为空时,front指向队列的第一个元素,rear指向队列最后一个元素的下一个位置。
- 当队列为空时,front=rear
- 队列满时:(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