鏈表基礎及常見面試題

基礎知識

鏈表是一種很常見的數據結構,在每個節點都保存了指向下一個節點的指針。與順序表相比,鏈表插入元素的複雜度是O(1),查找一個節點或者訪問特定節點編號的元素的複雜度是O(n);順序表插入元素的複雜度是O(n),而查找的複雜度是O(1)。使用鏈表可以不必事先知道數據的大小,但是增加了指針域,加大了內存的開銷。鏈表有三種類型:單向鏈表、雙向鏈表和循環鏈表。


鏈表節點的定義

typedef struct linkedNode
{
	int value;
	struct linkedNode* next;
}node,*pNode;

鏈表相關的基本操作(創建和遍歷)

/*
	創建鏈表
*/
pNode create(int* a,int n)
{
	int i;
	pNode pHead = (pNode) malloc(sizeof(node));
	if(pHead == NULL)
	{
		exit(0);
	}

	pNode pTail = pHead;
	pTail->next = NULL;

	for(i = 0;i < n;i++)
	{
		pNode nNode = (pNode) malloc(sizeof(node));
		if(nNode == NULL)
		{
			exit(0);
		}
		nNode->value = a[i];
		pTail->next = nNode;
		nNode->next = NULL;
		pTail = nNode;
	}
	return pHead;
}

/*
	遍歷鏈表
*/
void travel(pNode head)
{
	pNode tmp = head->next;
	while(tmp != NULL)
	{
		printf("%d ", tmp->value);
		tmp = tmp->next;
	}
	printf("\n");
}

插入數據、刪除數據等操作較爲簡單且在本文中暫時用不着,所以在此不一一贅述了。


常見面試題分析

接下來,就下面幾個問題進行分別討論

Q1 鏈表的倒數第k個節點

Q2 鏈表是否存在環

Q3 鏈表的中間節點

Q4 鏈表的反序

Q5 鏈表的排序

Q6 僅給定一個非尾節點的節點,刪除該節點

Q7 僅給定一個非空節點,在該節點後插入一個節點


Q1 鏈表的倒數第k個節點

注意:在這裏的倒數第k個節點是從1開始計數,如鏈表節點有{3,5,7,1,4,6},倒數第3個節點指的就是值爲1的節點。

假設鏈表有n個節點,倒數第k個節點,也即第n - k + 1個節點。僅遍歷一次鏈表的解法是,定義兩個指針。第一步,第一個指針從頭指針開始遍歷到第k - 1個元素,第二個指針不動;第二步,兩個指針同時移動,當第一個指針移到尾指針時,第二個指針正好指向了倒數第k個節點。

/*
	找到鏈表的倒數第k個節點
	鏈表節點{3,5,7,1,4,6},倒數第3個節點是值爲1的節點
*/
pNode findKthToTail(pNode head,int k)
{
	if(head == NULL || k <= 0)
	{
		return NULL;
	}

	int i,j;
	pNode pA = head;
	pNode pB = head;

	for(i = 0;i < k - 1;i++)
	{
		if(pA->next != NULL)
		{
			pA = pA->next;
		}
		else
			return NULL;
	}

	while(pA->next != NULL)
	{
		pA = pA->next;
		pB = pB->next;
	}

	return pB;
}


Q2 鏈表是否存在環

判斷一個鏈表是否存在環,可以定義兩個指針,一個每次移動一步,另一個每次移動兩步。如果每次移動兩步的指針指向了NULL,則說明不存在環;如果兩個指針相遇,則說明存在環。

/*
	判斷鏈表是否存在環
*/
bool isExitCycle(pNode head)
{
	if(head == NULL)
	{
		return false;
	}

	pNode pA = head;
	pNode pB = head;

	while(pB->next != NULL && pB != NULL)
	{
		pB = pB->next->next;
		pA = pA->next;

		if(pA == pB)
		{ // 若兩個指針相遇,則存在環
			return true;
		}
	}

	return false;
}

Q3 鏈表的中間節點

與判斷上一個問題鏈表是否存在環的問題類似,我們可以定義兩個指針,一個每次移動一步,另一個每次移動兩步。當每次移動兩步的指針指向了鏈表的尾節點時,每次移動一步的指針正好處於中間位置,即我們要找的中間節點位置。當然,這是在不知道鏈表的長度,且爲了減少時間複雜度的解法。

/*
	求鏈表的中間節點
*/
pNode getCenterPoint(pNode head)
{
	if (head == NULL || head->next == NULL) //如果鏈表爲空,或僅有頭節點
	{
		return head;
	}

	pNode pA = head;
	pNode pB = head;

	while(pB != NULL && pB->next != NULL)
	{
		pA = pA->next;
		pB = pB->next->next;		
	}
	return pA;
}


Q4 鏈表的反序

求一個鏈表的反序,需要注意的是不能讓鏈表斷開,爲此需要三個節點,分別用來保存現節點,節點的前向元素,節點的後向元素。

/*
	反轉鏈表
*/
pNode reverse(pNode head)
{
	pNode rHead = NULL;
	pNode mNode = head;
	pNode mPre = NULL;

	while(mNode != NULL)
	{
		pNode mNext = mNode->next;

		if(mNext == NULL) //當位於原鏈表尾節點時,將最後一個節點設爲新鏈表的頭節點
		{
			rHead = mNode;
		}

		mNode->next = mPre;
		mPre = mNode;
		mNode = mNext;
	}
	return rHead;
}

Q5 鏈表的排序

對鏈表進行排序可以採用冒泡、選擇、插入等多種方法,這裏採用選擇排序來對鏈表進行排序。具體其他排序方法的思想,可以參考這裏

/*
	鏈表的選擇排序
*/
void select_sortLink(pNode head)
{
	pNode p,q,min;
	int tmp;

	p = head->next;

	while(p->next != NULL)
	{
		min = p;
		q = p->next;
		while(q->next != NULL)
		{
			if(q->value < min->value)
			{
				min = q;
			}
			q = q->next;
		}
		if(min != p)
		{
			tmp = p->value;
			p->value = min->value;
			min->value = tmp;
		}
		p = p->next;
	}
}


Q6 僅給定一個非尾節點的節點,刪除該節點

在不知道頭指針的情況下,要刪除一個特定的節點,難以獲取該節點的前一個節點,所以將該節點的值設置爲下一個節點的值,然後將下一個節點刪除即可。

/*
	在不知鏈表頭指針的情況下,刪除節點p。將節點p的下一個節點的值賦給p,然後刪除p
*/
void deletePoint(pNode mNode)
{
	if(mNode == NULL)
	{
		return;
	}

	pNode tmp = mNode->next;
	if(tmp == NULL)
	{
		mNode =  NULL;
	}
	else
	{
		mNode->value = tmp->value;
		mNode->next = tmp->next;
		delete tmp;
	}
}

Q7 僅給定一個非空節點,在該節點前插入一個節點

與Q6類似,將新的節點插在該節點後,然後將新的節點的值與該節點的值交換即可。
/*
	將節點q插在節點p前
*/
void insertToPre(pNode p,pNode q)
{
	if(p == NULL || q == NULL)
	{
		return;
	}

	q->next = p->next;
	p->next = q;

	int tmp = q->value;
	q->value = p->value;
	p->value = tmp;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章