1. 程式人生 > >演算法面試題:如何判斷單鏈表中是否存在環

演算法面試題:如何判斷單鏈表中是否存在環

題目分析

一道演算法面試題:判斷單鏈表是否存在環

我們知道單鏈表中結點都是一個結點指向下一個結點這樣一個一個連結起來的,直到尾結點的指標域沒有指向,單鏈表就到此結束了。

這裡存在環的意思就是,尾結點的指標域併為空,而是指向此單鏈表的其他結點,這樣就形成環了,這樣遍歷單鏈表就死迴圈了因為破壞了結束條件。

前面我們演算法面試題 快速找到單鏈表中間節點 所用的快慢指標,同樣可以用來判斷是否存在環,如果存在環的,快慢指標一定會相遇

當然可以用其他方法,不過這種方法比較容易想到和比較容易理解,今天我們來講解這種方法。注意下面的單鏈表是帶頭結點的單鏈表


人為構造有環的單鏈表

很簡單,將正常的單鏈表的尾結點的next指標域指向單鏈表的其他結點

看程式碼:

//loop_pos 設定環結點的位置
Status SetLoopLinkList(LinkList list,int loop_pos)
{
	if (list == NULL|| LengthLinkList(list) == 0 || loop_pos >= LengthLinkList(list))
	{
		return ERROR;
	}
	//頭結點
	Node* node =  list;
	//第loop_pos個結點
	Node* loop_node = NULL;
	int i = 0;
	while (1)
	{

		if (node->next != NULL)
		{
			node = node->next;
			i++;
			if (i == loop_pos)
			{
				loop_node = node;
			}
		}
		else
		{
			break;
		}
	}
	//將尾結點指向連結串列中其他結點
	node->next = loop_node;
	return OK;
}
看一下設定環的單鏈表的展示情況:

展示單鏈表的資訊會環的位置一直迴圈


快慢指標判斷單鏈表是否環

看程式碼:

/*
使用快慢指標判斷單鏈表是否存在環。
使用slow、fast 2個指標,slow慢指標每次向前走1步,fast快指標每次向前走2步,
若存在環的話,必定存在某個時候 slow = fast 快慢指標相遇。
list 帶頭結點的單鏈表
返回值 >0:存在環返回環的位置	0:不存在環
*/
int IsLoopLinkList(LinkList list)
{
	//空指標
	if (list == NULL)
	{
		return 0;
	}
	//只有頭結點,沒有元素
	if (list->next == NULL)
	{
		return 0;
	}
	Node* slow = list;
	Node* fast = list;
	int loc = 0;
	while (1)
	{
		if (fast->next == NULL)
		{
			//快指標 到底連結串列尾結點說明 沒有環,此時slow 指向中間結點
			return 0;
		}
		else
		{
			if (fast->next != NULL && fast->next->next != NULL)
			{
				fast = fast->next->next;
				slow = slow->next;
			}
			else
			{
				fast = fast->next;
			}
		}
		//某個時刻 快慢指標相遇,說明此處存在環!
		if (slow == fast)
		{
			return (slow - list) / sizeof(Node);
		}

	}
	return 0;
}

那麼問題來了,既然知道該單鏈表是程式碼單鏈表,那麼我怎樣將其還原為正常單鏈表呢?

帶環單鏈表去掉環

我們只需要將尾結點的next指標域置為NULL就還原為正常單鏈表了。我們怎樣找到尾結點呢?請看程式碼。

/*
去掉單鏈表中的環,將尾結點的next域置位空。
環位置結點之後的結點,判斷其next域是否為環結點,如果是環節點,說明是尾結點
*/
Status ClearLoopLinkList(LinkList list)
{
	int loopLoc = IsLoopLinkList(list);//環結點位置
	Node* node = list;
	Node* loop = NULL;
	if (loopLoc)
	{
		int num = 0;

		while (node->next != NULL)
		{
			node = node->next;
			num++;
			if (num == loopLoc)
			{
				loop = node;
			}
			if (num > loopLoc && node->next == loop)
			{
				//找到尾結點,尾結點指標域置位空
				node->next = NULL;
				break;
			}
		}
	}
	return OK;
}

最後驗證結果

我們測試下,人為設定環,判斷單鏈表是否有環,做清除環,看遍歷是否正常。


一切操作正常。

所有程式碼

在之間單鏈表的演算法實現基礎上做的,程式碼有點多,做了下分檔案。

LinkList.h

#pragma once
#ifndef __LINK_LIST_H__
#define __LINK_LIST_H__
#define	ERROR 0
#define OK 1
typedef int EleType;
typedef int Status;
typedef struct Node Node;

//連結串列元素
struct Node
{
	EleType data;//資料域
	Node * next;//指標域
};
//帶頭結點的單鏈表
typedef Node* LinkList;
void PrintLinkList(LinkList list);
/*
建立擁有頭結點的連結串列
頭插法建立連結串列,往連結串列中新增元素,新建立的的元素始終在頭結點後面類似 頭指標 -> 頭結點->An->An-1->An-2 ...-> A1
元素的資料 隨機生成100以內的數字
*/
Status CreatLinkListInHead(LinkList *list, int n);

//尾插法建立連結串列,往連結串列中新增元素,先新增的元素放前面,後新增的元素放後面,遵循先來後到的道理
Status CreatLinkListInTail(LinkList *list, int n);

//獲取連結串列元素資料,通過指標返回 連結串列第position個元素的 資料,position從1開始
Status GetELement(LinkList list, int position, EleType *e);

//往連結串列中第position位置 插入元素,元素資料為 e,元素位置從1開始數
Status InsertLinkList(LinkList *list, int position, EleType e);

//在連結串列第position位置 刪除元素,通過指標返回 刪除元素的資料內容
Status DelLinkListEle(LinkList *list, int position, EleType *e);

//清空整個連結串列,釋放指標指向的記憶體
Status FreeLinkList(LinkList *list);

//獲取單鏈表元素個數
int LengthLinkList(LinkList list);

/*
給單鏈表設定環,尾結點 next域本應該指向NULL,讓其指向連結串列中的其他元素,使之形成有環的單鏈表。
list:單鏈表
loop_pos:形成環的位置,也就是尾指標指向單鏈表中的第幾個結點
*/
Status SetLoopLinkList(LinkList list,int loop_pos);

/*
將去掉單鏈表中的環
*/
Status ClearLoopLinkList(LinkList list);
#endif // !__LINK_LIST_H__

LinkList.c

#include <stdio.h>
#include <Windows.h>
#include "LinkList.h"
void PrintLinkList(LinkList list) {
	if (NULL ==list) {//連結串列為空
		printf("printLinkList error \n");
		return;
	}
	int i = 1;
	LinkList li = list->next;//從頭結點後面第一個結點開始遍歷
	while (li)//最後一個元素沒有指向,不會進行迴圈。
	{
		printf("第%d元素:%d\t", i, li->data);
		li = li->next;
		if (i % 5 == 0)
		{
			printf("\n");
			Sleep(3000);
		}
		i++;
		
	}
	printf("\n");
	return;
}
//建立擁有頭結點的連結串列
//頭插法建立連結串列,往連結串列中新增元素,新建立的的元素始終在頭結點後面類似 頭指標 -> 頭結點->An->An-1->An-2 ...-> A1
//元素的資料 隨機生成100以內的數字
Status CreatLinkListInHead(LinkList *list,int n) {
	srand(time(0));//設定隨機種子,為產生隨機數做準備。
	//LinkList 型別 和 Node * 型別是一樣的!看最上方定義類型別名,為什麼可以做呢?連結串列的頭指標 就是指向結點Node,所有連結串列型別是 Node *,只是為了更好看而已。 
	LinkList li=NULL;
	Node *node=NULL;//可以定義為 LinkList *node = NULL;
	int i = 0;
	li = (LinkList)malloc(sizeof(Node));//頭指標指向頭結點,給頭結點分配空間
	if (NULL == li||n<0) {//分配記憶體空間失敗或者連結串列元素個數非法 返回
		return ERROR;
	}
	li->next = NULL;//初始頭結點沒有指向
	for ( i = 0; i < n; i++)
	{
		node = (Node*)malloc(sizeof(Node));//給結點分配空間
		if (NULL==node) {//給分配空間失敗 
			return ERROR;
		}
		node->data = rand()%100;//除以100取餘的到100以內的數字,不包括100,如果含100 就需要+1
		node->next = li->next;//新新增的元素指標域 指向頭結點後面的結點
		li->next = node;//頭結點指標域 指向新增加的元素

	}
	*list = li;//通過指標 給外部連結串列賦值
	return OK;
}
//尾插法建立連結串列,往連結串列中新增元素,先新增的元素放前面,後新增的元素放後面,遵循先來後到的道理
Status CreatLinkListInTail(LinkList *list, int n) {
	srand(time(0));//設定隨機種子
	LinkList li = NULL ;
	Node *node = NULL;
	int i = 0;
	li = (LinkList)malloc(sizeof(Node));
	if (NULL == li || n<0) {//分配記憶體空間失敗或元素個數非法
		return ERROR;
	}
	li->next = NULL;//初始頭結點指向
	*list = li;//給通過外部連結串列賦值,指向頭結點,此時 list 和 li 都指向頭結點。
	for ( i = 0; i < n; i++)
	{
		node = (Node*)malloc(sizeof(Node));//給結點分配空間
		if (NULL == node) {//分配空間失敗 
			return ERROR;
		}
		node->data = rand() % 100;//除以100取餘的到100以內的數字,不包括100,如果含100 就需要+1
		li->next = node;//新結點 放到連結串列末尾
		li = node;//移動連結串列指標指向最新連結串列最後一個元素,為了下次迴圈在連結串列末尾新增元素
		//temp = node;
	}
	//最後表尾元素指標域 設定NULL
	li->next = NULL;
	//*list = li;//一定要放在for迴圈前面,不然頭結點的記憶體空間沒有指向!起初l指向頭結點,但是進入for迴圈後l的指向發生改變要移動指向最新新增的結點元素。
	return OK;
}
//獲取連結串列元素資料,通過指標返回 連結串列第position個元素的 資料,position從1開始
Status GetELement(LinkList list, int position,EleType *e) {
	//異常情況:空指標,元素位置非法
	if (NULL == list || position < 1) {
		return ERROR;
	}
	LinkList li = list;
	int i = 1;
	while (li && i<position) {//在連結串列範圍內遍歷元素 直到 找到第position位置的 元素  
		i++;
		li = li->next;
	}
	if (NULL == li || position > i) {//超出連結串列範圍,都遍歷完連結串列還沒找到元素。
		return ERROR;
	}
	//通上面 while迴圈 當i = position 跳出迴圈 ,li 此時指向連結串列 第position位置的 前面一個結點。
	*e = li->next->data;
	return OK;
}
//往連結串列中第position位置 插入元素,元素資料為 e,元素位置從1開始數
Status InsertLinkList(LinkList *list,int position,EleType e) {
	Node *node = (Node*)malloc(sizeof(Node));
	//異常情況:空指標,為插入元素分配空間失敗,元素位置非法,
	if (NULL == list || NULL==node || position < 1) {
		return ERROR;
	}
	LinkList li = * list;
	int i = 1;
	while ( li && i<position) {//在連結串列範圍內遍歷元素 直到 找到第position位置的 元素  
		i++;
		li = li->next;
	}
	if (NULL == li || position > i) {//超出連結串列範圍
		return ERROR;
	}
	//通上面 while迴圈 當i = position 跳出迴圈 ,li 此時指向連結串列 第position位置的 前面一個結點。
	node->data = e;
	node->next = li->next;//讓 插入結點指標域指向 原來位置的結點
	li->next = node;//讓插入結點位置前的結點 指向插入結點
	return OK;
}
//在連結串列第position位置 刪除元素,通過指標返回 刪除元素的資料內容
Status DelLinkListEle(LinkList *list, int position, EleType *e) {
	//異常情況:空指標,元素位置非法
	if (NULL == list || position < 1) {
		return ERROR;
	}
	LinkList li = *list;
	Node * node = NULL;
	int i = 1;
	while (li && i<position) {//在連結串列範圍內遍歷元素 直到 找到第position位置的 元素  
		i++;
		li = li->next;
	}
	if (NULL == li || position > i) {//超出連結串列範圍,都遍歷完連結串列還沒找到元素。
		return ERROR;
	}
	//通上面 while迴圈 當i = position 跳出迴圈 ,li 此時指向連結串列 第position位置的 前面一個結點。
	node = li->next;//儲存要刪除元素的地址
	li->next = node->next;//讓刪除元素前置結點 指向 刪除元素 直接後繼結點。然後就可以釋放 刪除結點的空間了。
	*e = node->data;//將刪除元素資料通過指標修改返回
	free(node);//釋放刪除元素空間
	return OK;
}
//清空整個連結串列,釋放指標指向的記憶體
Status FreeLinkList(LinkList *list) {
	if (NULL == list)//空指標
		return ERROR;
	//注意:頭結點不要釋放!只釋放頭結點後面的結點元素
	//LinkList li = *list;這樣將會把頭結點也釋放掉
	LinkList li = (*list)->next;//將連結串列指標指向第一個結點元素,從這個元素開始釋放
	Node * node = NULL;//臨時變數,指向還未釋放的元素
	while (li){//連結串列指標向移動指向結點元素,直到沒有後繼結點
		node = li->next;//儲存要釋放結點 的直接後繼結點位置。 
		free(li);
		li = node;//繼續指向 未釋放結點
	}
	//將頭結點指標域設定為NULL
	(*list)->next = NULL;
	return OK;
}
//獲取單鏈表元素個數
int LengthLinkList(LinkList list)
{
	Node* node = list;
	if (node == NULL)
	{
		return 0;
	}
	if (node->next == NULL)
	{
		return 0;
	}
	int length = 0;
	while (node->next!=NULL)
	{
		length++;
		node = node->next;
	}
	return length;
}
//loop_pos 設定環結點的位置
Status SetLoopLinkList(LinkList list,int loop_pos)
{
	if (list == NULL|| LengthLinkList(list) == 0 || loop_pos >= LengthLinkList(list))
	{
		return ERROR;
	}
	//頭結點
	Node* node =  list;
	//第loop_pos個結點
	Node* loop_node = NULL;
	int i = 0;
	while (1)
	{

		if (node->next != NULL)
		{
			node = node->next;
			i++;
			if (i == loop_pos)
			{
				loop_node = node;
			}
		}
		else
		{
			break;
		}
	}
	//將尾結點指向連結串列中其他結點
	node->next = loop_node;
	return OK;
}
/*
使用快慢指標判斷單鏈表是否存在環。
使用slow、fast 2個指標,slow慢指標每次向前走1步,fast快指標每次向前走2步,
若存在環的話,必定存在某個時候 slow = fast 快慢指標相遇。
list 帶頭結點的單鏈表
返回值 >0:存在環返回環的位置	0:不存在環
*/
int IsLoopLinkList(LinkList list)
{
	//空指標
	if (list == NULL)
	{
		return 0;
	}
	//只有頭結點,沒有元素
	if (list->next == NULL)
	{
		return 0;
	}
	Node* slow = list;
	Node* fast = list;
	int loc = 0;
	while (1)
	{
		if (fast->next == NULL)
		{
			//快指標 到底連結串列尾結點說明 沒有環,此時slow 指向中間結點
			return 0;
		}
		else
		{
			if (fast->next != NULL && fast->next->next != NULL)
			{
				fast = fast->next->next;
				slow = slow->next;
			}
			else
			{
				fast = fast->next;
			}
		}
		//某個時刻 快慢指標相遇,說明此處存在環!
		if (slow == fast)
		{
			return (slow - list) / sizeof(Node);
		}

	}
	return 0;
}
/*
去掉單鏈表中的環,將尾結點的next域置位空。
環位置結點之後的結點,判斷其next域是否為環結點,如果是環節點,說明是尾結點
*/
Status ClearLoopLinkList(LinkList list)
{
	int loopLoc = IsLoopLinkList(list);//環結點位置
	Node* node = list;
	Node* loop = NULL;
	if (loopLoc)
	{
		int num = 0;

		while (node->next != NULL)
		{
			node = node->next;
			num++;
			if (num == loopLoc)
			{
				loop = node;
			}
			if (num > loopLoc && node->next == loop)
			{
				//找到尾結點,尾結點指標域置位空
				node->next = NULL;
				break;
			}
		}
	}
	return OK;
}

main.c

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include "LinkList.h"

/*
單鏈表中尾結點的next指標為空代表著單鏈表結束了。
單鏈表中有環,就是尾結點的next域併為為空,指向了單鏈表中其他的結點從而形成了環。
*/
int main(int argc, char *argv[])
{
	LinkList list = NULL;
	CreatLinkListInTail(&list, 6);
	printf("單鏈表元素個數:%d\n", LengthLinkList(list));
	//正常連結串列的展示
	printf("正常單鏈表:\n");
	PrintLinkList(list);
	//printf("帶環的單鏈表展示:\n");
	SetLoopLinkList(list, 3);
	int loc = IsLoopLinkList(list);
	printf("環的位置:%d\n", loc);
	ClearLoopLinkList(list);
	printf("清除帶環單鏈表的環\n");
	PrintLinkList(list);

	
	return 0;
}