鏈表操做基礎

鏈表的基礎操做咱們都很熟悉,與此同時更流暢,完整,正確的解決,也是必不可少的基本功。所以我對它們作了一個總結,千萬不要小看這些基礎操做,熟悉它們之後你解決鏈表的問題將會駕輕就熟,那麼,開始吧!node

先給出鏈表原型,後續的全部過程都基於這個結構體。算法

struct ListNode {
    int val;
    struct ListNode *next;
};
複製代碼

四種基本操做:安全

建立,插入,刪除,反轉less

  • Create
  • Insert
  • Remove
  • Reverse

1.Create

頭插法

struct ListNode* createList(int n){
	struct ListNode* head = NULL;
	for (int i = 0; i < n; ++i)
	{
		struct ListNode* p = (struct ListNode*)malloc(sizeof(struct ListNode));
		//Q1
		if(!p)
			return NULL;
		//Q2
		p->val = i;
		p->next = head;
		head = p;
	}
	return head;
}
複製代碼

要生成一個長度爲n的鏈表,每一個循環裏,new 一個p節點,讓它的下一個節點指向鏈表頭,迭代,將這個p節點更新爲鏈表頭,返回這個鏈表頭函數

頭插法的特色在於你插的最後一個節點鏈表頭,意思是你順序訪問鏈表時將獲得一個和插入序列相反的序列。測試

Q1:爲何檢查p?這樣操做有風險嗎?ui

由於動態內存分配存在失敗的可能,當失敗後返回的p是一個空指針,那麼接下來兩行對p的間接訪問都將報錯。this

即便這樣處理,依然存在一個問題,那就是咱們是在某一次循環過程當中malloc失敗,咱們讓函數返回,意味着整個createList函數失敗,而且咱們在前面成功分配的那部份內存並無回收,咱們甚至都沒有一個手段訪問到那些已經成功分配了的內存,那麼這個函數就會像一個黑洞,每一次失敗的調用,都會產生一部分空間的浪費,內存泄漏就極有可能會發生。所以,更周全的作法也許是在分配失敗時,咱們在函數內部free掉以前已經的內存。spa

不過好在這種狀況,在咱們寫到的函數裏基本不會發生,咱們能夠放心使用上面的作法生成一個鏈表。指針

Q2:有其餘的賦值方法嗎?最好方便測試?

固然,咱們能夠根據咱們對鏈表的操做對它進行修改。 例如咱們想對測試鏈表排序,咱們可讓i換成rand。 咱們想要刪除鏈表重複元素,咱們可讓它等於i / 3,這樣會產生連續三個相同值的節點。 咱們想產生1-2-3-4-5而不是5-4-3-2-1那可讓它變成n-i

尾插法

struct ListNode* createList(int n){
	struct ListNode* head = (struct ListNode*)malloc(sizeof(struct ListNode));
	struct ListNode* tail = head;
	for (int i = 0; i < n; ++i)
	{
		struct ListNode* p = (struct ListNode*)malloc(sizeof(struct ListNode));
		if(!p)
			return NULL;
		p->val = i;
		p->next = NULL;
		tail->next = p;
		tail = p;
	}
    //Q1
	return head->next;
}
複製代碼

與頭插法相反,尾插法將每個新的p節點看成是鏈表的尾節點,它們的next域通通初始化爲NULL,迭代的是tail,每一次插入後讓tail指向新的p,對照着頭插法,你很快會弄清楚尾插的原理。

Q1:爲何返回的是head->next?

由於咱們第一次循環時,tail實際上同時也是head,這時的這第一個p應該就是咱們所但願返回的鏈表頭,head->next即是這一個p,因此我返回head->next。

2.Insert

插入的關鍵在於找到待插入位置以前的那個節點,改變它的next域,讓它指向插入的新節點p。所以,特殊的位置每每發生在鏈表頭,它以前沒有節點了,是NULL。咱們須要對這種特殊狀況考慮。固然,若是一個鏈表有一個val域無心義的頭節點dummyHead,這個問題也就不存在了。下面的即是一個有頭節點的例子,注意,有頭節點的鏈表在訪問時都要先往前推動一個。

咱們來看LeetCode上關於基礎操做的一道題:707.Design Linked List.

題目描述過長我就不引用了,上面連接過去看吧。

個人解答:

MyLinkedList* myLinkedListCreate() {
    MyLinkedList* p = (MyLinkedList*)malloc(sizeof(MyLinkedList));
    p->val = -1;
    p->next = NULL;
    return p;
}

int myLinkedListGet(MyLinkedList* obj, int index) {
	if(index < 0)
		return -1;
	if(!obj->next)
		return -1;
	MyLinkedList* p = obj->next;
    for (int i = 0; i < index; ++i)
    {
    	p = p->next;
    	if(!p)
    		return -1;
    }
    return p->val;
}

void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
	MyLinkedList* p = (MyLinkedList*)malloc(sizeof(MyLinkedList));
	if(!p)
		return;
	p->val = val;
	p->next = obj->next;
	obj->next = p;   
}

void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
    MyLinkedList* p = obj;
    while(p->next){
    	p = p->next;
    }
    MyLinkedList* newp = (MyLinkedList*)malloc(sizeof(MyLinkedList));
    if(!newp)
		return;
    newp->val = val;
    newp->next = NULL;
    p->next = newp;
}

void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
	if(index < 0)
		return;
	MyLinkedList* p = obj;
    for (int i = 0; i < index; ++i)
    {
    	p = p->next;
    	if(!p)
    		return;
    }
    MyLinkedList* newp = (MyLinkedList*)malloc(sizeof(MyLinkedList));
    if(!newp)
		return;
    newp->val = val;
    newp->next = p->next;
    p->next = newp;
}

void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
    if (index < 0)
    	return;
    MyLinkedList* tmp, *p = obj;
    if(!obj->next)
    	return;
    for (int i = 0; i < index; ++i)
    {
    	//ensure the delete node is not null
    	if(!p->next->next)
    		return;
    	p = p->next;
    }
    //p is before the node tobe deletes 
    //normal
    tmp = p->next;
    p->next = p->next->next;
    free(tmp);

}

void myLinkedListFree(MyLinkedList* obj) {
	MyLinkedList* tmp;
	while(obj){
		tmp = obj;
		obj = obj->next;
		free(tmp);
	}   
}

void myLinkedListPrint(MyLinkedList* obj){
	MyLinkedList* p = obj->next;
	while(p){
		printf("%d->", p->val);
		p = p->next;
	}
	printf("NULL\n");
}
複製代碼

3.Remove

其實刪除或許徹底能夠和插入放在一塊兒,它的重點仍然是找到它的前一個節點,若是刪除的是第一個元素,那麼這是一種特殊狀況,它的前面沒有節點,應該單獨考慮。

那麼不把它們放在一塊兒的緣由是什麼呢?刪除涉及到空間的回收,也就是free的正確使用,這將是一個很容易出錯的地方。關於free, 我會在最後再提到它。

例子能夠參考上面代碼裏的myLinkedListDeleteAtIndex()

來作幾道題吧!

203.Remove Linkedlist Elements
Problems
Remove all elements from a linked list of integers that have value val.

Example: Input: 1->2->6->3->4->5->6, val = 6 Output: 1->2->3->4->5

第一種作法,定義一個顯示的pre指針,並保證它的指向始終指向刪除元素的前一個位置,那麼顯然,對第一個元素的刪除是一種特殊狀況。

struct ListNode* removeElements_2(struct ListNode* head, int val) {
	struct ListNode* cur = head, *pre = NULL, *tmp;
	while(cur != NULL){
		if(cur->val == val){			
			if(pre != NULL)
				pre->next = cur->next;			
			else
				head = head->next;	
			tmp = cur;						
			cur = cur->next;
			free(tmp);	
		} else{
			pre = cur;
			cur = cur->next;
		}		
	}	
	return head;   
}
複製代碼

對於這種作法,我在第7行對這種狀況進行了處理。那就是改變head的指向而不是改變pre->next,由於你沒法對一個NULL指針進行->運算,由於他實際上包含了指針的間接訪問,pre->next = (*pre).next,這一點你在使用->它的時候應該很清楚纔是。

關於空間釋放,這裏要釋放的固然是cur節點,由於要刪除的就是它,那麼爲何不直接free(cur)呢?free(cur)以後,咱們沒法訪問cur->next,由於它已經被釋放掉了,那咱們怎麼推動cur呢?答案是設置一個臨時指針,先將以前的cur節點記錄下來,咱們在訪問事後,釋放那個tmp節點,就很是安全了。

另外一種作法能夠避免刪除頭元素的特殊狀況,它使用到了指針的指針&運算符,取地址運算符也算是C的一大特性了,同時,這個例子也是指針的指針的經典運用

struct ListNode* removeElements(struct ListNode* head, int val) {
	struct ListNode ** p = &head, * tmp;
	while(*p){
		if((*p)->val == val){
			tmp = *p;
			*p = (*p)->next;
			free(tmp);
		}
		else
			p = &((*p)->next);
	}
	return head;   
}
複製代碼

該算法中用來推動的是*p,咱們發如今刪除時咱們改變了*p,這實際上操縱的就是當前元素,意思是咱們的前一個元素無需作出改變,依然指向它原先指向的節點,而咱們對這個節點直接作出改變,讓它變成它的下一個節點,再釋放掉它,刪除就完成了。那麼爲何它能夠避免刪除頭元素的特殊狀況呢?由於咱們根本就不用考慮前一個節點了。

要改變一個指針p的內容咱們經過*p實現,那麼要改變一個指針p,咱們須要使用*pp,其中pp = &p,pp實際上就是一個指針的指針。

那麼像p1 = p2這樣又表明着什麼呢?爲了弄清楚上面的內容,再舉一個例子。

int a = 3, b = 2;
a = b;
b = 4;
複製代碼

如今a = ?顯然 a = 2。 只是把b當前的值賦給了a。p1 = p2一樣也只是讓p2當前的值賦給了p2,p1和p2之間在這行語句後就沒什麼聯繫了。

再來一道, 83.Remove Duplicates from Sorted List II

Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numbers from the original list.

Example 1:
Input: 1->2->3->3->4->4->5 Output: 1->2->5

Example 2:
Input: 1->1->1->2->3 Output: 2->3

個人解答:

struct ListNode* deleteDuplicates(struct ListNode* head) {
    if(!head || !head->next)
        return head;
    struct ListNode* fakeNode = (struct ListNode*)malloc(sizeof(struct ListNode));
    fakeNode->next = head;
    struct ListNode* pre = fakeNode, *tmp;
    //pre是第一個uinique元素出現的前一個節點
    while(head){
        //釋放了這些節點的內存,推動了head,但沒有改變指向
        while(head->next && head->val == head->next->val){
            tmp = head;
            head = head->next;
            free(tmp);
        }
        //說明head是unique的,直接往前推動
        if(pre->next == head){
            pre = pre->next;
            head = head->next;
        }
        //一次性改變pre->next,pre不推動
        else{
            pre->next = head->next;
            free(head);
            head = pre->next;
        }
    }
    return fakeNode->next;    
}
複製代碼

這一樣是一個增長頭節點避免特殊狀況的例子,fakeNode就是這個頭節點。
須要注意的是,我採起的策略裏並非每刪除一個元素就改變它前一個節點的指向,而是在推動到不等於當前連續的這些值時,一次性改變pre->next讓它直接指向下一個,也就是值開始不一樣的那個節點。

關於刪除,leetcode上還有這些題能夠參考:237,83,707,82。

4.Reverse

關於反轉鏈表,我會很天然的想到頭插法,那麼的確這樣,你徹底能夠用頭插法的視角來看待下面這個例子。

206. Reverse Linked List

Reverse a singly linked list.

Example:

Input: 1->2->3->4->5->NULL Output: 5->4->3->2->1->NULL

struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* tmpHead = NULL;
    struct ListNode* p = head;
    while(p != NULL){
    	struct ListNode* tmp = p->next;
    	p->next = tmpHead;
    	tmpHead = p;
    	p = tmp;
    }
    return tmpHead;
}
複製代碼

很簡單,對不對,就是遍歷原鏈表,再將節點依次用頭插法的方式插入。值得注意的是,如今鏈表的結構已經改變,咱們作的是in-place,也就是在原有空間上作出的修改,咱們如今的head已經變成了什麼?

事實上,觀察一個鏈表的結構有沒有被破壞,咱們只用看對鏈表的訪問過程當中咱們有沒有對p->next有沒有改變,即便p不是head自己,在本例中咱們第一次循環時便改變了它,這個時候head->next變成了NULL,對,它變成了尾節點。改變p對原鏈表的結構其實是沒有影響的。想一想爲何?

再看一例:
25. Reverse Nodes in k-Group

Given a linked list, reverse the nodes of a linked list k at a time and return its modified list. k is a positive integer and is less than or equal to the length of the linked list. If the number of nodes is not a multiple of k then left-out nodes in the end should remain as it is.

Example: Given this linked list: 1->2->3->4->5 For k = 2, you should return: 2->1->4->3->5 For k = 3, you should return: 3->2->1->4->5

思路:一次選擇k個元素一組,這一組中第一個元素設爲first,而後將這個組反轉kHead是這個反轉子串的頭,設立一個pre指針,始終指向反轉後最後一個元素,開始時pre指向一個dummyHead,也就是這裏的head2。每次循環結束前讓pre->next指向新的kHead

個人解答:

struct ListNode* reverseKGroup(struct ListNode* head, int k) {
	int length = 0; 
	struct ListNode* p = head;
	while(p){
		p = p->next;
		length += 1;
	}	
	if(length < k)
		return head;
	int res_node = length;
	struct ListNode* head2 = (struct ListNode*)malloc(sizeof(struct ListNode)); 
	struct ListNode* pre = head2;
	struct ListNode* kHead = NULL, *tmp, *first = NULL;
	while(res_node >= k){
		int n = k;
		kHead = NULL;
		first = NULL;
		while(n--){
			first = first?first:head;
			tmp = head->next;
			head->next = kHead;
			kHead = head;
			head = tmp;
		}
		pre->next = kHead;
		pre = first;
		res_node -= k;
	}
	pre->next = head;
	return head2->next;
}
複製代碼

dummyHead真香~

更多關於reverse的還有成對交換啊,指定位置反轉啊,利用反轉花式改變鏈表結構啊,利用反轉判斷迴文啊,leetcode上都有對應的題目,能夠按tag去搜索挑戰一下。

相關文章
相關標籤/搜索