一个问题:判断单项链表中是否有环,若有环,找出环的汇点
方法1:
每次访问一个节点,记录下地址信息,存放于set中,每次访问下一个节点,查看该节点是否已经存在,若存在,则该节点是构成环的关键点
输入: list
输出:若有环,返回构成环的关键点,若没有,返回NULL
pCurNode = list.head
set<listNode*> visistedSet
while (pCurNode->next)
pCurNode = pCurNode->next
bool ret = checkVisitedSet(pCurNode, visitedSet) //检查该节点是否已经访问过
if ret = true then
return pCurNode;
end if
visitedSet.add(pCurNode)
end while
return NULL;
空间:O(N) 记录访问过的每个节点地址
时间:外层循环N次,内部检查visitedSet所花时间如下:
1 0
2 lg(1)
3 lg(2)
.
.
.
N lg(N-1)
时间复杂度为:lg(1) + lg(2) + lg(3) + ...... lg(N-1) = lg((N-1)!) 若采用HASH表,则在visitedSet中查找时间为1,那么时间复杂度为O(N)
方法2
2.1 判断是否有环
设置两个指针,一个指针每次移动一个步长,另外一个指针每次移动2个步长,若没有环,则两个节点在访问的过程中都会变成NULL; 若有环,则两个指针会在环中不停的移动,由于两者步长不一样,一定会相遇。
我们认为相遇也只是感性的认识,在一个圈内,一个速度快,一个速度慢,肯定能碰头。 下面我们分析一下,为什么一定为相遇。只要保证,一定会相遇,我们才能用这种方法,进行判断链表是否有环。 先做如下参数定义: 1.链表节点数量为N 链表设定顺序[1......N] (头节点不算) 2.环汇点的下表为c 3.圈长:N - c + 1 4.设此处slow走了s步,则fast走了2s步 相遇时slow下标为:(分两种情况,走的步长未超过链表长度/超过链表长度)
s (s <= N)
c + (s - c)%(N - c + 1) (s > N)
fast下标为
2s (2s <= N)
c + (2s - c)%(N - c + 1) (2s > N)
当两者相遇时,下标肯定是相等的。
第一种组合: s = 2s s = 0
第二种情况: s = c + (2s - c)%(N - c + 1) (N/2 <= s <= N)
第三种情况: c + (s - c)%(N - c + 1) = 2s (s > N && 2s <= N) 无意义
第四种情况: c + (s - c)%(N - c + 1) = c + (2s - c)%(N - c + 1) (s > N)
针对上面四种情况,我们只需要证明第二种情况和第四种情况,若方程肯定有解,说明两个指针肯定能够相遇,
若无解,则两个指针不可能相遇,该方法无效。
对于第二种情况:即 (s - c) = (2s - c)%(N - c + 1) ( N/2 <= s <= N)
即 2s - c = k * (N - c + 1) + s - c (k为1.2.3.......) s = k * (N - c + 1)方程有解。
对于第四种情况: 去c,得到 (s - c)%(N - c + 1) = (2s - c)%(N - c + 1) (s > N)
设余数为 f
s - c = (N - c + 1) * m + f
2s - c = (N - c + 1) * n + f
则s = (N - c + 1)*(n - m) (m,n 为正整数 && n >m)
综上所述:一定存在这样的步长s = (N - c + 1)*k (s >= N/2 & s >= c)使得快慢指针相遇。
有了上述说明后,我们就可以放心大胆的使用这个方法,去证明链表中有没有环,如果有环,那么我们一定可以找到。
checkCircle操作:
listNode* checkCircle(list* pList)
{
assert(pList);
assert(pList->head);
//起点相同,开始起跑
listNode* pOneStep = pList->head;
listNode* pTwoStep = pList->head;
while (pOneStep && pTwoStep)
{
pOneStep = pOneStep->next;
pTwoStep = pTwoStep->next;
if (pTwoStep) pTwoStep = pTwoStep->next;
if (pOneStep == pTwoStep)
return pOneStep;
}
return NULL;
}
2.2 找汇点位置
在网络上看别人的博客,常常会提到这么一句话:“找到环的入口点 当fast若与slow相遇时,slow肯定没有走遍历完链表,而fast已经在环内循环了n圈(1<=n)”,就是说在slow指针走完一圈内肯定能相遇,但是都没有说为什么.
此处用反证法:即slow指针不可能在一圈范围内相遇,即不存在正整数k,满足 (s>= c && s <= N)
c <= (N - c + 1) * k <= N
c - 1 < (N - c + 1) * k < N + 1
(c - 1) / (N - c + 1) < k < (N + 1)/ (N - c + 1)
再看区间范围: (N + 1)/ (N - c + 1) - (c - 1) / (N - c + 1) = (N - c + 2) / (N - c + 1) > 1 !!!
区间范围是大于1的,一定可以取一个整数k,满足上述不等式,所以假设不成立。
那么slow指针一定可以在一圈范围内就和fast指针相遇。
还利用之前证明得到的关系求汇点位置:
s = (N - c + 1) * k;
s = (k - 1)*(N - c + 1) + N - c + 1
c = (k - 1)*(N - c + 1) + N - s + 1
c为汇点位置, N - c + 1为环的长度, N - s + 1为相遇点到环汇点的距离
那就是说用一个指针指向头节点,用一个指针指向相遇节点,依次往前走,一定会在汇点出相遇。寻找汇点方法:
listNode* findPoint(list* pList)
{
assert(pList);
assert(pList->head);
listNode* meetNode = checkCircle(pList);
if (!meetNode) return NULL;
listNode* p = pList->head;
listNode* q = meetNode;
while (p != q)
{
p = p->next;
q = q->next;
}
return p;
}
测试程序
链表结构定义:
typedef struct listNode
{
struct listNode *next;
int key;
}listNode;
typedef struct list
{
listNode* head;
listNode* tail;
int len;
}list;
API说明
list* createEmptyList();
void listRelease(list* pList);
void addNodeInHead(list* pList, int key);
void addNodeInTail(list* pList, int key);
listNode* searchNode(list* pList, int key);
listNode* checkCircle(list* pList);
listNode* findPoint(list* pList);
void showList(list* pList);
我们主要关注:
测试代码:
int main(int argc, char *argv[])
{
//生成链表
list* pList = createEmptyList();
for (int i = 0; i < 100; i++)
addNodeInTail(pList, i);
//在节点40处圈个环
listNode* pCircleNode = searchNode(pList, 40);
pList->tail->next = pCircleNode;
//判断是否有环
listNode* pNode = checkCircle(pList);
if (!pNode)
printf("No Circle\n");
else
printf("Node Value: %d\n", pNode->key);
//找汇点
listNode* pointNode = findPoint(pList);
if (!pointNode)
printf("No Point\n");
else
printf("\nPointNode Value: %d\n", pointNode->key);
listRelease(pList);
getchar();
return 0;
}
测试结果: 如图所示,在测试程序中设置了再节点40处构成环,查找确实找到了节点40为汇点
在节点59处,两个指针相遇,并找到环的汇点40。