LinkedList鏈表(從酒店管理到單鏈表)

今天說說鏈表:說到鏈表就不得不提及數組,兩者相愛相殺,但又都是極爲重要的基本數據結構類型。

相對於鏈表,我們一般情況下更熟悉數組。

聽說加上英文,會顯得高端不少):

先講一個小故事(雖然講的很爛2333)

從前,一羣小朋友外出遊玩,到酒店申請房間啊,他們呢,申請的是數組方式的房間(哈哈)那麼他們的房間號就是相連的。他們很容易的相互串門,帶隊者也可以輕鬆的找到他們每個人的所在(帶隊者只用記住第一個小朋友的位置和學號就好)。但問題是,他們這樣申請房間的話,有的小朋友如果申請更換房間,會比較麻煩,更有甚者,不小心生病了,需要退房,那麼負責人就要重新給每個人好分配房間,大多數小朋友都需要更換自己的房間,就會很麻煩。這時候,酒店管理者想到,如果我們讓學號相鄰的小朋友的記住他(她)下一個學號的小朋友的房間號(就像一號記住二號的房間號,二號記住三號的房間號),那麼負責人還是隻用記住第一個小朋友的房間號就可以按照順序依次找到每個小朋友。(當然要是其中一個小朋友忘記後面小朋友的位置的話。。。我們就失去了一堆小朋友)。酒店管理者發現這樣的話,不僅可以使得他們的房間利用率得到大大的提升,並且可以方便住宿人員的調動。

好的,相信讀完這個尷尬的小故事,我們大概對鏈表有了一個感性的認識,他是爲了解決一些數組的不足而出現的。下面就是嚴謹的分析鏈表的優缺點。

作爲有強大功能的鏈表,對他的操作當然有許多,比如:鏈表的創建,修改,刪除,插入,輸出,排序,反序,清空鏈表的元素,求鏈表的長度等等。

初學鏈表,一般從單向鏈表開始:(本文也暫時只有單向鏈表)

Advantages 優勢

1) Dynamic size(動態大小)

2) Ease of insertion/deletion(方便插入和刪除)

Drawbacks 缺點

1) Random access is not allowed. We have to access elements sequentially starting from the first node. So we cannot do binary search with linked lists efficiently with its default implementation.

不允許隨機訪問。 我們必須從第一個節點開始順序訪問元素。 因此,我們無法使用其默認實現對鏈接列表進行有效的二分查找。

2) Extra memory space for a pointer is required with each element of the list.

需要額外的空間去存放鏈表中下一個元素的位置。

3) Not cache friendly. Since array elements are
contiguous locations, there is locality of reference which is not there
in case of linked lists.

不緩存友好。 由於數組元素是連續的位置,因此存在引用位置,而在鏈接列表的情況下則不存在。

(就是說不能寫成a[6])

Representation 表達(內容)

首先,我們將一個鏈表中的每個元素稱爲一個節點(Node),特殊的第一個被叫做頭節點,最後一個被叫做尾節點。(圖中,abcd是四個節點,但注意a不是頭節點哈哈,Head是頭節點,他指向的a節點)一般來說,頭節點Head不存放內容,尾節點沒有下一個元素的位置。(以簡單的單鏈表爲例)

在這裏插入圖片描述
A linked list is represented by a pointer to the first node of the
linked list. The first node is called the head. If the linked list is
empty, then the value of the head is NULL.

鏈接列表由指向鏈接列表的第一個節點的指針表示。 第一個節點稱爲頭。 如果鏈表爲空,則head的值爲NULL。(好繞口
Each node in a list consists of at least two parts:

每一個節點至少包括兩部分:

  1. data 數據
  2. Pointer (Or Reference) to the next node 指向下一個結點的指針(指向/引用)
    In C, we can represent a node using structures. Below is an example of a linked list node with integer data.

在c語言中,我們使用結構體來表示,如下面的代碼:

struct Node {
int data;
struct Node* next;
};

In Java or C#, LinkedList can be represented as a class and a Node as a
separate class. The LinkedList class contains a reference of Node class
type.

而在java,c#中LinkedList可以表示爲一個類,而Node可以表示爲單獨的類。 LinkedList類包含Node類類型的引用。

class LinkedList { 
	Node head; // head of the list 

	/* Linked list Node*/
	class Node { 
		int data; 
		Node next; 

		// Constructor to create a new node 
		// Next is by default initialized 
		// as null 
		Node(int d) { data = d; } 
	} 
}

寫一下鏈表?

我們從c開始嘗試:(現在只寫到了增)

好的,在此之前,我們似乎好像可能看起來學會了如何使用,但是看懂了和會寫出來中間還是有一定的差距的。

鏈表還有一下基礎的知識需要去掌握:

malloc :用於申請一定的內存.

那麼這句(struct Node*)malloc(sizeof(struct Node))是什麼鬼呢?(這也太長了吧

不急,我們仔細看看,他是申請了一個內存,多大呢?(struct Node)類型的所需要的儲存空間;(就像房間的類型一樣,偏要雙人牀的那種)

那麼前面的那個(struct Node*)是?哦,他是說這一塊申請來的內存要強制轉化成struct Node類型的,就是說專門給節點使用的。(自己辛辛苦苦申請來的,纔不給別人用呢)。

對了,這句話是會返回一個地址的,就是指向自己申請內存的位置。

注意:節點呀!只有Head和普通節點。我們說的頭指針,尾指針一類的,都是僅僅聲明,作用就像房卡一樣,是可以換的,裏面存放的是鏈表中節點的地址。

好的,下面我們要耐住性子,認真的學習咯(後文的故事如果聽不太懂,放心你不是一個人,多看看不是故事的內容會更好);(下面是一個標準化的介紹)

// A simple C program to introduce 
// a linked list 
#include <stdio.h> 
#include <stdlib.h> 

//這個結構體就是我們要用到的節點了,
//既然c語言裏的基本類型沒有,我們就自己造一個。
struct Node { 
	int data; 
	struct Node* next; 
}; 

// Program to create a simple linkedlist with 3 nodes 
//編程去建立一個有三個節點的鏈表
int main() 
{ 
	//聲明3個節點,注意了哈,這裏是聲明!!!
	//並沒用得到那三個節點,就像我們拿到了三個空門牌,但是房間還沒造出來呢
	struct Node* head = NULL; 
	struct Node* second = NULL; 
	struct Node* third = NULL; 

	// allocate 3 nodes in the heap
	//從堆中申請三個節點的內存(這會兒纔得到房間了,並把房間號寫在了門牌上) 
	head = (struct Node*)malloc(sizeof(struct Node)); 
	second = (struct Node*)malloc(sizeof(struct Node)); 
	third = (struct Node*)malloc(sizeof(struct Node)); 

	/* Three blocks have been allocated dynamically. 
	We have pointers to these three blocks as head, 
	second and third	 
	下面是靈魂的繪圖(佩服佩服)來表示我們所申請的三個節點的儲存狀態,
	他們是不相連的哦
	head		        second	   	 third 
		|		         	 |		    	   | 
		|	          	 |	     		   |
	+---+-----+	 +----+----+	 +----+----+ 
	| # | # |	    | # | # |	    | # | # | 
	+---+-----+	 +----+----+	 +----+----+ 
	#代表了隨機值,一個是數據,一個是指針,
	和我們的stuct node結構相同哦,
	但是他們現在還是隨機值,因爲我們沒有給他們賦值。
	
	# represents any random value. 
	Data is random because we haven’t assigned 
	anything yet */

	head->data = 1; // assign data in first node 
	head->next = second; 
	// Link first node with the second node 
	// 我們給第一個節點賦值了,並且告訴了他下一個節點的位置

	/* data has been assigned to the data part of the first 
	block (block pointed by the head). And next 
	pointer of first block points to second. 
	So they both are linked. 
	//那個代碼由於複製可能會造成錯位,木得辦法,撮合着看把哈哈。
		head		 second		 third 
			|			 |			 | 
			|			 |			 | 
	+---+---+	 +----+----+	 +-----+----+ 
	| 1 | o----->| # | # |	 | # | # | 
	+---+---+	 +----+----+	 +-----+----+	 
	*/

	// assign data to second node 
	second->data = 2; 

	// Link second node with the third node 
	second->next = third; 

	//同上,給第二個賦值,並且告訴他下一個在哪裏
	/* data has been assigned to the data part of the second 
	block (block pointed by second). And next 
	pointer of the second block points to the third 
	block. So all three blocks are linked. 
	
	head		 second		 third 
		|			 |			 | 
		|			 |			 | 
	+---+---+	 +---+---+	 +----+----+ 
	| 1 | o----->| 2 | o-----> | # | # | 
	+---+---+	 +---+---+	 +----+----+	 */

	third->data = 3; // assign data to third node 
	third->next = NULL; 
	
	//給第三個(也是最後一個)賦值,並告訴他,你後面沒人了。
	/* data has been assigned to data part of third 
	block (block pointed by third). And next pointer 
	of the third block is made NULL to indicate 
	that the linked list is terminated here. 

	We have the linked list ready. 

		head	 
			| 
			| 
		+---+---+	 +---+---+	 +----+------+ 
		| 1 | o----->| 2 | o-----> | 3 | NULL | 
		+---+---+	 +---+---+	 +----+------+	 

	Note that only head is sufficient to represent 
	the whole list. We can traverse the complete 
	list by following next pointers. */
	
	//接下來一定要試試效果麼!
	struct Node* n = head;
	//新建立一個指針,讓他去循環着跑
	//並且哈,它指向了第一個有數據的節點
	while (n != NULL) { 
        printf(" %d ", n->data); 
        n = n->next; //輸出完後,他就有指向了下一個
    }
	return 0; 
}

這就完了?這就完了。

下面是一個簡潔的代碼:

// A simple C program for traversal of a linked list 
#include <stdio.h> 
#include <stdlib.h> 
  
struct Node { 
    int data; 
    struct Node* next; 
}; 
  
// This function prints contents of linked list starting from 
// the given node 
void printList(struct Node* n) 
{ 
    while (n != NULL) { 
        printf(" %d ", n->data); 
        n = n->next; 
    } 
} 
  
int main() 
{ 
    struct Node* head = NULL; 
    struct Node* second = NULL; 
    struct Node* third = NULL; 
  
    // allocate 3 nodes in the heap 
    head = (struct Node*)malloc(sizeof(struct Node)); 
    second = (struct Node*)malloc(sizeof(struct Node)); 
    third = (struct Node*)malloc(sizeof(struct Node)); 
  
    head->data = 1; // assign data in first node 
    head->next = second; // Link first node with second 
  
    second->data = 2; // assign data to second node 
    second->next = third; 
  
    third->data = 3; // assign data to third node 
    third->next = NULL; 
  
    printList(head); 
  
    return 0; 
}

當然,當我自己上手之後,生活還是狠狠地把我按在地上摩擦。。。

不妨我們模塊化的寫一下一個鏈表吧(寫了很久來理解鏈表的優秀)

我先分開講解每個模塊的作用和意義,之後放在一起來觀看,效果~~(可能)~~更佳。

1 必不可少的這個結構體呀!

你看,這個stuct Node *多長呀,如果我們使用的是typedef的話我們就可以將l作爲一個數據類型的名字,然後把l當作int這樣的來使用就好了(我們還是可以使用struct Node的)。

其次,看第二行,這個調用就很有意思了,聲明瞭一個l數據類型的指針。

typedef struct Node
{
    int val;
    l* next;//等效 struct Node *next
}l;

2 我們要創建一個鏈表了。

我們在main函數裏面聲明瞭一個Head的指針,並且申請了一個內存空間,並把其地址放進了head。這個時候把他傳過來。

或者說,我們要了一個房卡,並要了一個房間,這個房卡就是head,房卡對應的就是該房間。

第一步:

在creat函數裏面:我們先是malloc 了一個l類型的空間,這個房間的地址賦值給了新聲明的n指針上。【或者說我們向酒店索要了一間l類型的房間(房卡當然比房間要好拿到一些呀),同時把這個房間的門牌號輸入到了門卡上。】

第二步:我們把指針n的內容(也就是新聲明的節點的地址)賦值給head指針指向的next的位置【哈哈,有點懵吧,就是說,我們現在手裏一共有兩張房卡對吧,一張是head(head房卡可是能打開head房間的哦),另一張是n(n可是上面有房間號碼的),然後我們打開head的房間,在裏面的next區域放上n房卡的內容(就是剛剛申請的房間號/地址)(用這個例子可能好理解哈,指針總是很奇妙的)

第三步:將n賦值,並指向空【然後我們利用n房卡打開剛剛申請的房間哈,然後在內容區域放上內容,在下一個的區域放上空,說明後面沒有房間了】

這就好了。想象一下我們拿着head房卡就可以了,先是到head的next區域找到下一個房間的位置,然後進去,就能看見第一個房間的內容區域還能看到下一個房間在哪裏了,當然在此處我們沒有下一個房間。

void creat(l *head){
    l *n = (l*)malloc(sizeof(l));
    if(n==NULL)cout<<"Error,malloc failed!";//這裏做一下判斷,如果內存不足,那麼報錯
		head->next = n;
    n->val = 6;
    n->next = NULL;   
}

3那我們一定要再來一個房間呀

第一步:傳入一個頭節點,和一個值【拿上head門卡和要往新房間裏放的東西】

第二步:聲明一個新的節點p,並使其和head指向相同。【再要一張門卡,複製一下head門卡的內容】這樣做可以保證head不被破環。

第三步:開始找最後一個房間了,如果指向的房間中的next區域還有值,就說明後面還有房間,我們就把這個門卡p指向本來房間裏的下一個房間位置,就是我們將p門卡本來打開時的是這個房間,我們使他打開的是下一個房間,(當然,他無法再打開這個房間了)然後繼續搜索。直到發現只是最後一個房間。

第四步:我們新索要一個房間,原來最後一個空房間的next區域要存放上這個房間的位置了。然後我們把我們的東西也就是一個值放在新的房間,這個新房間就成了最後一個房間了,我們就需要把他的next變爲NUll空,記住哦,這些步驟一步也不能缺少,否則就會釀成大錯。。。(無限卡殼

void add(l*Head,int val){   
    l* p = Head;
    while(p->next)p=p->next;
    l*t = (l*)malloc(sizeof(l));
		p->next =t;
    t->val=val;
    t->next = NULL;
    }

4趕緊巡查一遍房間吧:

我們只用拿着head門卡就好,每次巡查的時候記得再要一個門卡自己的門卡保存着第一個房間的位置,可不能亂改。然後搜查完成一間房間之後,房卡n就成了下一個房間的房卡,繼續搜查。哈哈

void disp(struct Node *head){
    struct Node *n = head->next;
    while(n){
        cout<<n->val<<" ";
        n = n->next;
    }
    cout<<endl;
}

總的代碼如下:

#include <bits/stdc++.h>
using namespace std;
typedef struct Node
{
    int val;
    struct Node* next;
}l;  

void disp(struct Node *head){
    struct Node *n = head->next;
    while(n){
        cout<<n->val<<" ";
        n = n->next;
    }
    cout<<endl;
}

void creat(l *head){
    l *n = (l*)malloc(sizeof(l));
    if(n==NULL)cout<<"Error,malloc failed!";
	head->next = n;
    n->val = 6;
    n->next = NULL;   
} 

void add(l*Head,int val){   
    l* p = Head;
    while(p->next)p=p->next;
    l*t = (l*)malloc(sizeof(l));
    p->next = t;
    t->val=val;
    t->next = NULL;

}
int main(){
    l *Head;
    Head = (l*)malloc(sizeof(l));
    
    creat(Head);
    add(Head,15);
    disp(Head);
    return 0;
}

只是寫出一個鏈表就少了很多趣味,增改刪查纔是硬道理!

當然了,在開始階段,我們還是隻以單鏈表爲例:

鏈表的增:

在這裏插入圖片描述

一、 在鏈表的末尾增加:(再重複的系統的講一下哈)

這個方法我們在上面已經認識到了,具體的思路就是我們通過頭節點找到最後一個節點,然後在她後面添加一個節點,鏈接上去就可以了。(我們手持一個head房卡,沿着房間不斷地走下去,找到最後的房間,在新開一間房間,然後我們在原來的最後一個房間的next的空間裏放上新開房間的位置就好了)

這裏假設我們已經有了一個鏈表了。

void addLast(l*Head,int val){   
    l* p = Head;//設置臨時的指針,用來指向不同的節點,實現在不影響head的情況下遍歷等操作
    while(p->next)p=p->next;
    l*t = (l*)malloc(sizeof(l));
    p->next = t;
    t->val=val;
    t->next = NULL;
}

二、在鏈表的頭部增加:

這個的做法似乎比上一個還要簡單呢,畢竟我們不用去一一尋找到最後一個了。

既然我們需要添加一個值,那麼新建一個節點(房間)是必不可少的了。然後將值放入,讓其指向本來的第一個節點,在讓head指向它就好。(不怕麻煩的話,也可以重新添加一個指針變量,作爲中間值,來進行操作)

void addHead(l*Head,int val){
    l* t = (l*)malloc(sizeof(l));
    t->val = val;
    t->next = Head->next;
    Head ->next = t;
}

三、在鏈表的中間插入:

這個可以說是前兩者的綜合版本了。我們要先找到這個節點,然後對他進行插入。

這裏就以給定一個值,作爲目標值,在其後面添加一個節點吧。查找的方法是先檢查一下它後面是否還有節點,如果有就判斷他的值是否符合,如果不符合就使指針指向下一個節點。這樣出來的結果只會有兩種,一種是沒有找到,也有可能是這個值就在最後一個節點裏面。

這裏做了兩個版本的給定值尋找插入方法,一種是在其之前,另一種是在其後。(兩者之間只需要一點點的代碼改動就可以了,在不同的地方已經用//註釋)

void insertBefore(l* Head,int target,int val){
    l* pre  = Head;//*
    l* p = Head;
    while(p->next){
        if(p->val==target)break;
        else {
             pre = p;//*
            p = p->next;
        }
    }
    if(p->next==NULL&&p->val!=target){
        cout<<"查找失敗"<<endl;
        return;
    }
    l*t = (l*)malloc(sizeof(l));
    t->val = val;
    pre->next = t;//*
    t->next = p; //*
}

void insertAfter(l* Head,int target,int val){
    l* p = Head;
    while(p->next){
        if(p->val==target)break;
        else {
            p = p->next;
        }
    }
    if(p->next==NULL&&p->val!=target){
        cout<<"查找失敗"<<endl;
        return;
    }
    l*t = (l*)malloc(sizeof(l));
    t->val = val;
     t->next = p->next;//*
     p->next = t;//*
}

對鏈表的刪

???如果前面的掌握了的話,那麼對鏈表的某個節點進行刪除自然不是問題。就直接放代碼了(思路和上面的insertBefore相同哦)(代碼其實也是參考上面的

void del(l* Head,int target){//這裏傳入頭節點和目標值就可以了
    l* pre  = Head;
    l* p = Head;
    while(p->next){
        if(p->val==target)break;
        else {
             pre = p;
            p = p->next;
        }
    }
    if(p->next==NULL&&p->val!=target){
        cout<<"查找失敗"<<endl;
        return;
    }
		pre->next = p->next;//是的,將insertAfter代碼的最後幾行更改爲這一行就好了。
		//(相當於將這個節點跳過去了)
	  free(p); //既然刪除了這個節點,那麼就把他釋放掉,節約內存。
}

對鏈表的改:

???如果前面的掌握了的話,那麼對鏈表的某個值進行更改自然不是問題。就直接放代碼了(思路和上面的insertAfter相同哦)(代碼其實也是參考上面的(人類的本質是復讀機)

void change(l* Head,int target,int val){
    l* p = Head;
    while(p->next){
        if(p->val==target)break;
        else {
            p = p->next;
        }
    }
    if(p->next==NULL&&p->val!=target){
        cout<<"查找失敗"<<endl;
        return;
    }
    p->val = val;//其實就是把insertAfter()最後的幾行代碼改爲這個(2333)
   
}

對鏈表的查:

我們不是一直在查找麼???(~~避免復讀,~~就不再贅述)

看到這裏,首先是一份敬佩,敬佩您能夠靜下心來一步一步的去嘗試,去探索,去思考。確實文章篇幅很長,需要一定的耐心去思考,並且鏈表理解起來確實不是很困難,但是如果是剛開始,去上手操作,自然還是漏洞百出,bug重重。但是隻要我們多嘗試,多敲代碼,縷清關係,明確指針自身的所在。那麼,我們的各個方面都會有一定的成長。

謝謝閱讀,本文中仍有許多的不足之處,還望交流指正。

對於後期,大家還可以去了解其他的鏈表形式,來加強對鏈表的使用。

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