資料結構之連結串列C語言實現以及使用場景分析
連結串列是資料結構中比較基礎也是比較重要的型別之一,那麼有了陣列,為什麼我們還需要連結串列呢!或者說設計連結串列這種資料結構的初衷在哪裡?
這是因為,在我們使用陣列的時候,需要預先設定目標群體的個數,也即陣列容量的大小,然而實時情況下我們目標的個數我們是不確定的,因此我們總是要把陣列的容量設定的很大,這樣以來就浪費了很多的空間。另外,陣列在進行插入操作和刪除操作的時候,在插入或者刪除制定元素之後,我們往往需要進行迴圈移位,這增加了我們的線性開銷。
正是由於以上的兩種主要原因,連結串列被設計出來用於一般表的操作。為了避免上面描述陣列的兩種弊端,我們希望連結串列有一下的特點
1 可以靈活的擴充套件自己的長度。
2 儲存地址不連續,刪除或者插入操作的時候不需要迴圈移位。
要實現以上兩個特點,我們需既要保證每個節點的獨立性,又要儲存相鄰兩個節點的聯絡。
為此,連結串列一般被設計為下面的形式。
Node--->Node---->Node
連結串列是由一個一個的節點組成的,可以方便和自由的插入未知個Node,前一個節點中用指標儲存著下一個節點的位置,這樣以來便順利的完成了我們對連結串列的兩點期望,但是唯一的缺點是增加了額外的空間消耗。
————————————————————————————————————————————————————————————————————————————
連結串列的定義:
連結串列的定義一般使用結構體,在看《資料結構與演算法分析》這本書的時候發現,書中頻繁的使用typedef的關鍵字,結果真的很棒不僅保持的程式碼的整潔程度,也讓我們在下面的編碼過程中少見了很多煩人的指標(當然指標還是一直存在的)。所以這裡也借用了書中的定義方法。
struct Node; typedef struct Node* PtrNode; typedef PtrNode Position; typedef PtrNode List; struct Node{ int Value; PtrNode Next; };
下面接著書寫一個建立連結串列的函式,輸入每個節點的值,直到這個值是-1的時候函式結束。
在這個裡面,我以前一直搞不明白為什麼需要定義三個Node *,現在終於瞭解了,最終還是複習了指標的內容明白的,這裡說一下指標實現連結串列對指標的操作很頻繁,需要比較紮實的掌握了指標之後,在來看連結串列會輕鬆很多。在下面的一段程式裡,我分別定義了head/p/tmp這三個指向節點結構體的指標,head的主要作用就像一個傳銷頭目,他會主動聯絡上一個下線p,然後他就什麼也不幹了,p接著去發展一個又一個的下線tmp,結果一串以head為首的連結串列就出來了。
起先,我總覺得有了head,為什麼還要p,這是因為如果直接使用head去指向下一個節點,head的位置也是不斷在移動的,即它永遠處於連結串列的尾端,這樣當我們返回連結串列的時候,其實是空值。所以,我們需要p這個中轉環節。(其實,這種做法在指標中非常普遍,大部分有返回指標型別的函式中,都會首先定義一個指標變數來儲存函式的傳入的引數,而不是對引數直接進行操作)。
/* 函式功能:建立一個連結串列 函式描述:每次輸入一個新的整數,即把新增加一個節點存放該整數, 當輸入的整數為-1時,函式結束。 */ List create() { int n=0; Position p,head,tmp; head=NULL; tmp=malloc(sizeof(struct Node)); if(tmp==NULL) { printf("tmp malloc failed!\n"); return NULL; } else { p=tmp; printf("please input the first node's message!\n"); scanf("%d",&(tmp->Value)); } while(tmp->Value!=-1) { n+=1; if(n==1) { head=p; tmp->Next=NULL; } else { p->Next=tmp; } p=tmp; tmp=malloc(sizeof(struct Node)); printf("please input the %d node!\n",n+1); scanf("%d",&(tmp->Value)); } p->Next=NULL; free(tmp); //free函式free掉的只是申請的空間,但是指標還是依然存在的。 tmp=NULL; return head; }
接下來,在寫一個刪除連結串列節點的函式,輸入一個整數然後遍歷連結串列節點,當連結串列節點的值與該整數相等的時候,即把該節點刪除。
在完成這個函式首先一定要把這個過程思考清楚,不可否認我之前是一個上來就敲程式碼的人,看了《劍指offer》感覺這種習慣是程式設計師的大忌,甚至還想寫一篇部落格,名字都想好了《程式設計師的自我修養之思考在前,程式碼在後》。其實想想也是,我們寫程式的目的是為了解決問題,而不是為了簡單的寫程式,純粹的讓程式跑起來大概只會在上學那會存在吧!真實的程式開發中需要考慮幾乎所有 能想到的實際問題,所以無論程式再下,一要學會先思考清楚,再下筆寫程式。
關於這個函式,我們要想到的是:
1 如果連結串列為空,我們該怎麼做,當然是直接返回。
2 如果要刪除的元素為頭節點該怎麼辦?
3 如果要刪除的元素為尾節點該怎麼辦?
當注意到以上三個部分,我們的程式就可能避免掉了輸入連結串列為空,程式直接崩潰的現象,也可以避免刪除元素值為頭節點時刪不掉的尷尬。我們的程式就有了一定的魯棒性。
下面著重考慮連結串列的刪除的實現:
list: Node_a->Node_b->Node_c->Node_d;
list tmp p
-------> tmp->Next=p->Next;
list: Node_a->Node_b----------->Node_d
free(p)
假設我們要刪除的節點為上圖的Node_c;假設我們能夠找到Node_c的前一個位置tmp和被刪除節點位置p的話;這個時候我們只需要執行tmp->Next=p->Next即可。
只要完成上面的分析以及考慮到各種情況,我們完成下面的程式碼就水到渠成了。
/* 函式功能:刪除連結串列中指定值的節點(如果存在多個,只刪除第一個) 本例中輸入一個整數,刪除連結串列節點值為這個整數的節點。 */ List DeleteNode(List list) { Position p,tmp; int value; if(list==NULL) { printf("The list is null,function return!\n"); return NULL; } else { printf("please input the delete Node's value:\n"); scanf("%d",&value); } p=list; if(p->Value==value) { list=p->Next; free(p); p=NULL; return list; } while(p!=NULL&&p->Value!=value) { tmp=p; p=p->Next; } if(p->Value==value) { if(p->Next!=NULL){ tmp->Next=p->Next; } else { tmp->Next=NULL; } free(p); p=NULL; } return list; }
關於連結串列的使用場景分析:
連結串列在程式開發中用到的頻率還是非常高的,所以在高階語言中往往會對連結串列進行一些實現,比如STL中list以及Java中也有類似的東西。在目前的伺服器端開發,主要運用連結串列來接收一些從資料中取出來的資料進行處理。
即使你不知道連結串列的底層實現,仍然可以成功的運用STL裡面的現成的東西。但是作為一個學習者,我覺得會使用和從底層掌握仍然是兩個不同的概念,linux之父說:“talk is less,show you code”。
以下的程式,用連結串列模擬了一個電話通訊錄的功能,包括新增聯絡人,查詢聯絡人,以及刪除聯絡人。
PS:關於魯棒性,程式中最大的危險是使用了gets這個函式,目前先保留使用gets,等待找到工作之後在做進一步的程式完善。
/************************************************************************** Programe: This is a phone list write by list The programe is just prictise for list Author: heat nan Mail:[email protected] Data:2015/07/27 **************************************************************************/ #include<stdio.h> #include<string.h> #include<stdlib.h> #define N 25 #define M 15 struct node; typedef struct node* p_node; typedef p_node List; typedef p_node Position; typedef struct node** PList; struct node{ char name[N]; char number[M]; Position next; }; int JudgeNameExist(List list,char* name); void AddPerson(PList list); void PrintList(List list); List FindPerson(List list); List FindPersonByName(List list,char* name); int AddPersonByName(PList list,List node); int DeletePersonByName(PList list,char* name); void DeletePerson(PList list); int main() { List list=NULL; Position p; char cmd[100]; while(1) { printf(" MAIN \n"); printf(" ******* 1 add a person *******\n"); printf(" ******* 2 show the phone list *******\n"); printf(" ******* 3 find from phone list *******\n"); printf(" ******* 4 delete from phone list *******\n\n\n"); printf("Please input the cmd number:\n"); gets(cmd); switch(cmd[0]) { case '1': AddPerson(&list); break; case '2': PrintList(list); break; case '3': FindPerson(list); break; case '4': DeletePerson(&list); break; default: printf("wrong cmd!\n"); break; } } return 0; } /* Function:判斷要新增的聯絡人名稱是否已經存在於電話簿中. Input: List 電話列表,name 要新增的聯絡人的姓名. Return: 已經存在返回1,不存在返回0. */ int JudgeNameExist(List list,char* name) { if(FindPersonByName(list,name)!=NULL) return 1; else return 0; } /* Function:根據輸入的姓名查詢聯絡人的資訊節點 Input: 要輸入的電話列表list,姓名name Return: 返回查詢到的節點 */ List FindPersonByName(List list,char* name) { while(list!=NULL) { if(strcmp(list->name,name)==0) break; list=list->next; } return list; } /* Function:根據姓名新增新的聯絡人到聯絡人列表 Input: 指向聯絡人列表地址的指標, 新使用者節點 Return: 新增成功返回1,新增失敗返回0 */ int AddPersonByName(PList list,List node) { if(node==NULL) { printf("the node is NULL!\n"); return 0; } if(*list==NULL) { *list=node; return 1; } List pHead=*list; while(pHead->next!=NULL) pHead=pHead->next; pHead->next=node; return 1; } void AddPerson(PList list) { Position tmp; Position p_head; tmp=(struct node*)malloc(sizeof(struct node)); char name[N]; char number[M]; if(tmp==NULL) { printf("malloc the tmp node failed in function add person!\n"); } else { printf("please input the name:\n"); gets(name); printf("please input the number:\n"); gets(number); strcpy(tmp->name,name); strcpy(tmp->number,number); tmp->next=NULL; } if(JudgeNameExist(*list,name)==1) { free(tmp); printf("the name have already exist!\n"); return; } AddPersonByName(list,tmp); } /* Function: 列印聯絡人列表 Input: 聯絡人列表 */ void PrintList(List list) { Position show; show=list; if(show==NULL) { return ; } printf("Now,we print the phone list:\n"); while(show!=NULL) { printf("Name:%s Number:%s\n",show->name,show->number); show=show->next; } } List FindPerson(List list) { char name[N]; Position pHead=list; printf("please input the name you will find:\n"); gets(name); Position node=FindPersonByName(list,name); if(node!=NULL) printf("find success! name-> %s number-> %s\n",node->name,node->number); else printf("find failed!\n"); return node; } /* Function:根據姓名刪除聯絡人 Input: 指向聯絡人地址的指標,聯絡人姓名 Output: 刪除成功返回1,失敗返回0 */ int DeletePersonByName(PList list,char* name) { if(*list==NULL||name==NULL) return 0; List pHead=*list; if(strcmp(pHead->name,name)==0) { *list=pHead->next; free(pHead); pHead->next==NULL; return 0; } List tmp=pHead->next; while(tmp!=NULL) { if(strcmp(tmp->name,name)==0) { pHead->next=tmp->next; free(tmp); tmp->next=NULL; return 1; } pHead=tmp; tmp=tmp->next; } return 0; } void DeletePerson(PList list) { List pHead=*list; if(pHead==NULL) { printf("there is no person you can delet\n"); return ; } char name[N]; printf("please input the name:\n"); gets(name); DeletePersonByName(list,name); }
————————————————————————————————————————————————————————
*************************************************************************************************************************************************
————————————————————————————————————————————————————————
連結串列概述
連結串列是一種常見的重要的資料結構。它是動態地進行儲存分配的一種結構。它可以根據需要開闢記憶體單元。連結串列有一個“頭指標”變數,以head表示,它存放一個地址。該地址指向一個元素。連結串列中每一個元素稱為“結點”,每個結點都應包括兩個部分:一為使用者需要用的實際資料,二為下一個結點的地址。因此,head指向第一個元素:第一個元素又指向第二個元素;……,直到最後一個元素,該元素不再指向其它元素,它稱為“表尾”,它的地址部分放一個“NULL”(表示“空地址”),連結串列到此結束。
連結串列的各類操作包括:學習單向連結串列的建立、刪除、 插入(無序、有序)、輸出、 排序(選擇、插入、冒泡)、反序等等。
單向連結串列的圖示:
---->[NULL]
head
圖1:空連結串列
---->[p1]---->[p2]...---->[pn]---->[NULL]
head p1->next p2->next pn->next
圖2:有N個節點的連結串列
建立n個節點的連結串列的函式為:
#include "stdlib.h" #include "stdio.h" #define NULL 0 #define LEN sizeof(struct student) struct student { int num; //學號 float score; //分數,其他資訊可以繼續在下面增加欄位 struct student *next; //指向下一節點的指標 }; int n; //節點總數 /* ========================== 功能:建立n個節點的連結串列 返回:指向連結串列表頭的指標 ========================== */ struct student *Create() { struct student *head; //頭節點 struct student *p1 = NULL; //p1儲存建立的新節點的地址 struct student *p2 = NULL; //p2儲存原連結串列最後一個節點的地址 n = 0; //建立前連結串列的節點總數為0:空連結串列 p1 = (struct student *) malloc (LEN); //開闢一個新節點 p2 = p1; //如果節點開闢成功,則p2先把它的指標儲存下來以備後用 if(p1==NULL) //節點開闢不成功 { printf ("\nCann't create it, try it again in a moment!\n"); return NULL; } else //節點開闢成功 { head = NULL; //開始head指向NULL printf ("Please input %d node -- num,score: ", n + 1); scanf ("%d %f", &(p1->num), &(p1->score)); //錄入資料 } while(p1->num != 0) //只要學號不為0,就繼續錄入下一個節點 { n += 1; //節點總數增加1個 if(n == 1) //如果節點總數是1,則head指向剛建立的節點p1 { head = p1; p2->next = NULL; //此時的p2就是p1,也就是p1->next指向NULL。 } else { p2->next = p1; //指向上次下面剛剛開闢的新節點 } p2 = p1; //把p1的地址給p2保留,然後p1產生新的節點 p1 = (struct student *) malloc (LEN); printf ("Please input %d node -- num,score: ", n + 1); scanf ("%d %f", &(p1->num), &(p1->score)); } p2->next = NULL; //此句就是根據單向連結串列的最後一個節點要指向NULL free(p1); //p1->num為0的時候跳出了while迴圈,並且釋放p1 p1 = NULL; //特別不要忘記把釋放的變數清空置為NULL,否則就變成"野指標",即地址不確定的指標 return head; //返回建立連結串列的頭指標 }
輸出連結串列中節點的函式為:
/* =========================== 功能:輸出節點 返回: void =========================== */ void Print(struct student *head) { struct student *p; printf ("\nNow , These %d records are:\n", n); p = head; if(head != NULL) //只要不是空連結串列,就輸出連結串列中所有節點 { printf("head is %o\n", head); //輸出頭指標指向的地址 do { /* 輸出相應的值:當前節點地址、各欄位值、當前節點的下一節點地址。 這樣輸出便於讀者形象看到一個單向連結串列在計算機中的儲存結構,和我們 設計的圖示是一模一樣的。 */ printf ("%o %d %5.1f %o\n", p, p->num, p->score, p->next); p = p->next; //移到下一個節點 } while (p != NULL); } }
單向連結串列的刪除圖示:
---->[NULL]
head
圖3:空連結串列
從圖3可知,空連結串列顯然不能刪除
---->[1]---->[2]...---->[n]---->[NULL](原連結串列)
head 1->next 2->next n->next
---->[2]...---->[n]---->[NULL](刪除後連結串列)
head 2->next n->next
圖4:有N個節點的連結串列,刪除第一個節點
結合原連結串列和刪除後的連結串列,就很容易寫出相應的程式碼。操作方法如下:
1、你要明白head就是第1個節點,head->next就是第2個節點;
2、刪除後head指向第2個節點,就是讓head=head->next,OK這樣就行了。
---->[1]---->[2]---->[3]...---->[n]---->[NULL](原連結串列)
head 1->next 2->next 3->next n->next
---->[1]---->[3]...---->[n]---->[NULL](刪除後連結串列)
head 1->next 3->next n->next
圖5:有N個節點的連結串列,刪除中間一個(這裡圖示刪除第2個)
結合原連結串列和刪除後的連結串列,就很容易寫出相應的程式碼。操作方法如下:
1、你要明白head就是第1個節點,1->next就是第2個節點,2->next就是第3個節點;
2、刪除後2,1指向第3個節點,就是讓1->next=2->next。
刪除指定學號的節點的函式為:
/* ========================== 功能:刪除指定節點 (此例中是刪除指定學號的節點) 返回:指向連結串列表頭的指標 ========================== */ struct student *Del (struct student *head, int num) { struct student *p1; //p1儲存當前需要檢查的節點的地址 struct student *p2; //p2儲存當前檢查過的節點的地址 if (head == NULL) //是空連結串列(結合圖3理解) { printf ("\nList is null!\n"); return head; } //定位要刪除的節點 p1 = head; while (p1->num != num && p1->next != NULL) //p1指向的節點不是所要查詢的,並且它不是最後一個節點,就繼續往下找 { p2 = p1; //儲存當前節點的地址 p1 = p1->next; //後移一個節點 } if(p1->num==num) //找到了。(結合圖4、5理解) { if (p1 == head) //如果要刪除的節點是第一個節點 { head = p1->next; //頭指標指向第一個節點的後一個節點,也就是第二個節點。這樣第一個節點就不在連結串列中,即刪除 } else //如果是其它節點,則讓原來指向當前節點的指標,指向它的下一個節點,完成刪除 { p2->next = p1->next; } free (p1); //釋放當前節點 p1 = NULL; printf ("\ndelete %ld success!\n", num); n -= 1; //節點總數減1個 } else //沒有找到 { printf ("\n%ld not been found!\n", num); } return head; }
單向連結串列的插入圖示:
---->[NULL](原連結串列)
head
---->[1]---->[NULL](插入後的連結串列)
head 1->next
圖7 空連結串列插入一個節點
結合原連結串列和插入後的連結串列,就很容易寫出相應的程式碼。操作方法如下:
1、你要明白空連結串列head指向NULL就是head=NULL;
2、插入後head指向第1個節點,就是讓head=1,1->next=NULL,OK這樣就行了。
---->[1]---->[2]---->[3]...---->[n]---->[NULL](原連結串列)
head 1->next 2->next 3->next n->next
---->[1]---->[2]---->[x]---->[3]...---->[n]---->[NULL](插入後的連結串列)
head 1->next 2->next x->next 3->next n->next
圖8:有N個節點的連結串列,插入一個節點(這裡圖示插入第2個後面)
結合原連結串列和插入後的連結串列,就很容易寫出相應的程式碼。操作方法如下:
1、你要明白原1->next就是節點2,2->next就是節點3;
2、插入後x指向第3個節點,2指向x,就是讓x->next=2->next,1->next=x。
插入指定節點的後面的函式為:
/* ========================== 功能:插入指定節點的後面 (此例中是指定學號的節點) 返回:指向連結串列表頭的指標 ========================== */ struct student *Insert (struct student *head, int num, struct student *node) { struct student *p1; //p1儲存當前需要檢查的節點的地址 if (head == NULL) //(結合圖示7理解) { head = node; node->next = NULL; n += 1; return head; } p1 = head; while(p1->num != num && p1->next != NULL) //p1指向的節點不是所要查詢的,並且它不是最後一個節點,繼續往下找 { p1 = p1->next; //後移一個節點 } if (p1->num==num) //找到了(結合圖示8理解) { node->next = p1->next; //顯然node的下一節點是原p1的next p1->next = node; //插入後,原p1的下一節點就是要插入的node n += 1; //節點總數增加1個 } else { printf ("\n%ld not been found!\n", num); } return head; }
單向連結串列的反序圖示:
---->[1]---->[2]---->[3]...---->[n]---->[NULL](原連結串列)
head 1->next 2->next 3->next n->next
[NULL]<----[1]<----[2]<----[3]<----...[n]<----(反序後的連結串列)
1->next 2->next 3->next n->next head
圖9:有N個節點的連結串列反序
結合原連結串列和插入後的連結串列,就很容易寫出相應的程式碼。操作方法如下:
1、我們需要一個讀原連結串列的指標p2,存反序連結串列的p1=NULL(剛好最後一個節點的next為NULL),還有一個臨時儲存變數p;
2、p2在原連結串列中讀出一個節點,我們就把它放到p1中,p就是用來處理節點放置順序的問題;
3、比如,現在我們取得一個2,為了我們繼續往下取節點,我們必須儲存它的next值,由原連結串列可知p=2->next;
4、然後由反序後的連結串列可知,反序後2->next要指向1,則2->next=1;
5、好了,現在已經反序一個節點,接著處理下一個節點就需要儲存此時的資訊:
p1變成剛剛加入的2,即p1=2;p2要變成它的下一節點,就是上面我們儲存的p,即p2=p。
反序連結串列的函式為:
/* ========================== 功能:反序節點 (連結串列的頭變成連結串列的尾,連結串列的尾變成頭) 返回:指向連結串列表頭的指標 ========================== */ struct student *Reverse (struct student *head) { struct student *p; //臨時儲存 struct student *p1; //儲存返回結果 struct student *p2; //源結果節點一個一個取 p1 = NULL; //開始顛倒時,已顛倒的部分為空 p2 = head; //p2指向連結串列的頭節點 while(p2 != NULL) { p = p2->next; p2->next = p1; p1 = p2; p2 = p; } head = p1; return head; }
對連結串列進行選擇排序的基本思想就是反覆從還未排好序的那些節點中,選出鍵值(就是用它排序的欄位,我們取學號num為鍵值)最小的節點,依次重新組合成一個連結串列。
我認為寫連結串列這類程式,關鍵是理解:head儲存的是第一個節點的地址,head->next儲存的是第二個節點的地址;任意一個節點p的地址,只能通過它前一個節點的next來求得。
單向連結串列的選擇排序圖示:
---->[1]---->[3]---->[2]...---->[n]---->[NULL](原連結串列)
head 1->next 3->next 2->next n->next
---->[NULL](空連結串列)
first
tail
---->[1]---->[2]---->[3]...---->[n]---->[NULL](排序後連結串列)
first 1->next 2->next 3->next tail->next
圖10:有N個節點的連結串列選擇排序
1、先在原連結串列中找最小的,找到一個後就把它放到另一個空的連結串列中;
2、空連結串列中安放第一個進來的節點,產生一個有序連結串列,並且讓它在原連結串列中分離出來(此時要注意原連結串列中出來的是第一個節點還是中間其它節點);
3、繼續在原連結串列中找下一個最小的,找到後把它放入有序連結串列的尾指標的next,然後它變成其尾指標;
對連結串列進行選擇排序的函式為:
========================== */ struct student *SelectSort (struct student *head) { struct student *first; //排列後有序鏈的表頭指標 struct student *tail; //排列後有序鏈的表尾指標 struct student *p_min; //保留鍵值更小的節點的前驅節點的指標 struct student *min; //儲存最小節點 struct student *p; //當前比較的節點 first = NULL; while(head != NULL) //在連結串列中找鍵值最小的節點 { //注意:這裡for語句就是體現選擇排序思想的地方 for (p = head, min = head; p->next != NULL; p = p->next) //迴圈遍歷連結串列中的節點,找出此時最小的節點 { if (p->next->num < min->num) //找到一個比當前min小的節點 { p_min = p; //儲存找到節點的前驅節點:顯然p->next的前驅節點是p min = p->next; //儲存鍵值更小的節點 } } //上面for語句結束後,就要做兩件事;一是把它放入有序連結串列中;二是根據相應的條件判斷,安排它離開原來的連結串列 //第一件事 if (first == NULL) //如果有序連結串列目前還是一個空連結串列 { first = min; //第一次找到鍵值最小的節點 tail = min; //注意:尾指標讓它指向最後的一個節點 } else //有序連結串列中已經有節點 { tail->next = min; //把剛找到的最小節點放到最後,即讓尾指標的next指向它 tail = min; //尾指標也要指向它 } //第二件事 if (min == head) //如果找到的最小節點就是第一個節點 { head = head->next; //顯然讓head指向原head->next,即第二個節點,就OK } else //如果不是第一個節點 { p_min->next = min->next; //前次最小節點的next指向當前min的next,這樣就讓min離開了原連結串列 } } if (first != NULL) //迴圈結束得到有序連結串列first { tail->next = NULL; //單向連結串列的最後一個節點的next應該指向NULL } head = first; return head; }
對連結串列進行直接插入排序的基本思想就是假設連結串列的前面n-1個節點是已經按鍵值(就是用它排序的欄位,我們取學號num為鍵值)排好序的,對於節點n在這個序列中找插入位置,使得n插入後新序列仍然有序。按照這種思想,依次對連結串列從頭到尾執行一遍,就可以使無序連結串列變為有序連結串列。
單向連結串列的直接插入排序圖示:
---->[1]---->[3]---->[2]...---->[n]---->[NULL](原連結串列)
head 1->next 3->next 2->next n->next
---->[1]---->[NULL](從原連結串列中取第1個節點作為只有一個節點的有序連結串列)
head
圖11
---->[3]---->[2]...---->[n]---->[NULL](原連結串列剩下用於直接插入排序的節點)
first 3->next 2->next n->next
圖12
---->[1]---->[2]---->[3]...---->[n]---->[NULL](排序後連結串列)
head 1->next 2->next 3->next n->next
圖13:有N個節點的連結串列直接插入排序
1、先在原連結串列中以第一個節點為一個有序連結串列,其餘節點為待定節點。
2、從圖12連結串列中取節點,到圖11連結串列中定位插入。
3、上面圖示雖說畫了兩條連結串列,其實只有一條連結串列。在排序中,實質只增加了一個用於指向剩下需要排序節點的頭指標first罷了。
這一點請讀者務必搞清楚,要不然就可能認為它和上面的選擇排序法一樣了。
對連結串列進行直接插入排序的函式為:
/* ========================== 功能:直接插入排序(由小到大) 返回:指向連結串列表頭的指標 ========================== */ struct student *InsertSort (struct student *head) { struct student *first; //為原連結串列剩下用於直接插入排序的節點頭指標 struct student *t; //臨時指標變數:插入節點 struct student *p,*q; //臨時指標變數 first = head->next; //原連結串列剩下用於直接插入排序的節點連結串列:可根據圖12來理解 head->next = NULL; //只含有一個節點的連結串列的有序連結串列:可根據圖11來理解 while(first != NULL) //遍歷剩下無序的連結串列 { //注意:這裡for語句就是體現直接插入排序思想的地方 for (t = first, q = head; ((q != NULL) && (q->num < t->num)); p = q, q = q->next); //無序節點在有序連結串列中找插入的位置 //退出for迴圈,就是找到了插入的位置,應該將t節點插入到p節點之後,q節點之前 //注意:按道理來說,這句話可以放到下面註釋了的那個位置也應該對的,但是就是不能。原因:你若理解了上面的第3條,就知道了 //下面的插入就是將t節點即是first節點插入到p節點之後,已經改變了first節點,所以first節點應該在被修改之前往後移動,不能放到下面註釋的位置上去 first = first->next; //無序連結串列中的節點離開,以便它插入到有序連結串列中 if (q == head) //插在第一個節點之前 { head = t; } else //p是q的前驅 { p->next = t; } t->next = q; //完成插入動作 //first = first->next; } return head; }
對連結串列進行氣泡排序的基本思想就是對當前還未排好序的範圍內的全部節點,自上而下對相鄰的兩個節點依次進行比較和調整,讓鍵值(就是用它排 序的欄位,我們取學號num為鍵值)較大的節點往下沉,鍵值較小的往上冒。即:每當兩相鄰的節點比較後發現它們的排序與排序要求相反時,就將它們互換。
單向連結串列的氣泡排序圖示:
---->[1]---->[3]---->[2]...---->[n]---->[NULL](原連結串列)
head 1->next 3->next 2->next n->next
---->[1]---->[2]---->[3]...---->[n]---->[NULL](排序後連結串列)
head 1->next 2->next 3->next n->next
圖14:有N個節點的連結串列氣泡排序
任意兩個相鄰節點p、q位置互換圖示:
假設p1->next指向p,那麼顯然p1->next->next就指向q,
p1->next->next->next就指向q的後繼節點,我們用p2儲存
p1->next->next指標。即:p2=p1->next->next,則有:
[ ]---->[p]---------->[q]---->[ ](排序前)
p1->next p1->next->next p2->next
圖15
[ ]---->[q]---------->[p]---->[ ](排序後)
圖16
1、排序後q節點指向p節點,在調整指向之前,我們要儲存原p的指向節點地址,即:p2=p1->next->next;
2、順著這一步一步往下推,排序後圖16中p1->next->next要指的是p2->next,所以p1->next->next=p2->next;
3、在圖15中p2->next原是q發出來的指向,排序後圖16中q的指向要變為指向p的,而原來p1->next是指向p的,所以p2->next=p1->next;
4、在圖15中p1->next原是指向p的,排序後圖16中p1->next要指向q,原來p1->next->next(即p2)是指向q的,所以p1->next=p2;
5、至此,我們完成了相鄰兩節點的順序交換。
6、下面的程式描述改進了一點就是記錄了每次最後一次節點下沉的位置,這樣我們不必每次都從頭到尾的掃描,只需要掃描到記錄點為止。 因為後面的都已經是排好序的了。
對連結串列進行氣泡排序的函式為:
/* ========================== 功能:氣泡排序(由小到大) 返回:指向連結串列表頭的指標 ========================== */ struct student *BubbleSort (struct student *head) { struct student *endpt; //控制迴圈比較 struct student *p; //臨時指標變數 struct student *p1,*p2; p1 = (struct student *) malloc (LEN); p1->next = head; //注意理解:我們增加一個節點,放在第一個節點的前面,主要是為了便於比較。因為第一個節點沒有前驅,我們不能交換地址 head = p1; //讓head指向p1節點,排序完成後,我們再把p1節點釋放掉 for (endpt = NULL; endpt != head; endpt = p) //結合第6點理解 { for (p = p1 = head; p1->next->next != endpt; p1 = p1->next) { if (p1->next->num > p1->next->next->num) //如果前面的節點鍵值比後面節點的鍵值大,則交換 { p2 = p1->next->next; //結合第1點理解 p1->next->next = p2->next; //結合第2點理解 p2->next = p1->next; //結合第3點理解 p1->next = p2; //結合第4點理解 p = p1->next->next; //結合第6點理解 } } } p1 = head; //把p1的資訊去掉 head = head->next; //讓head指向排序後的第一個節點 free (p1); //釋放p1 p1 = NULL; //p1置為NULL,保證不產生“野指標”,即地址不確定的指標變數 return head; }
有序連結串列插入節點示意圖:
---->[NULL](空有序連結串列)
head
圖18:空有序連結串列(空有序連結串列好解決,直接讓head指向它就是了。)
以下討論不為空的有序連結串列。
---->[1]---->[2]---->[3]...---->[n]---->[NULL](有序連結串列)
head 1->next 2->next 3->next n->next
圖18:有N個節點的有序連結串列
插入node節點的位置有兩種情況:一是第一個節點前,二是其它節點前或後。
---->[node]---->[1]---->[2]---->[3]...---->[n]---->[NULL]
head node->next 1->next 2->next 3->next n->next
圖19:node節點插在第一個節點前
---->[1]---->[2]---->[3]...---->[node]...---->[n]---->[NULL]
head 1->next 2->next 3->next node->next n->next
插入有序連結串列的函式為:
/* ========================== 功能:插入有序連結串列的某個節點的後面(從小到大) 返回:指向連結串列表頭的指標 ========================== */ struct student *SortInsert (struct student *head, struct student *node) { struct student *p; //p儲存當前需要檢查的節點的地址 struct student *t; //臨時指標變數 if (head == NULL) //處理空的有序連結串列 { head = node; node->next = NULL; n += 1; //插入完畢,節點總數加 return head; } p = head; //有序連結串列不為空 while(p->num < node->num && p != NULL) //p指向的節點的學號比插入節點的學號小,並且它不等於NULL { t = p; //儲存當前節點的前驅,以便後面判斷後處理 p = p->next; //後移一個節點 } if (p == head) //剛好插入第一個節點之前 { node->next = p; head = node; } else //插入其它節點之後 { t->next = node; //把node節點加進去 node->next = p; } n += 1; //插入完畢,節點總數加1 return head; }
綜上所述,連結串列的各類操作函式的完整程式碼如下:
#include "stdlib.h" #include "stdio.h" #define NULL 0 #define LEN sizeof(struct student) struct student { int num; //學號 float score; //分數,其他資訊可以繼續在下面增加欄位 struct student *next; //指向下一節點的指標 }; int n; //節點總數 /* ========================== 功能:建立n個節點的連結串列 返回:指向連結串列表頭的指標 ========================== */ struct student *Create() { struct student *head; //頭節點 struct student *p1 = NULL; //p1儲存建立的新節點的地址 struct student *p2 = NULL; //p2儲存原連結串列最後一個節點的地址 n = 0; //建立前連結串列的節點總數為0:空連結串列 p1 = (struct student *) malloc (LEN); //開闢一個新節點 p2 = p1; //如果節點開闢成功,則p2先把它的指標儲存下來以備後用 if(p1==NULL) //節點開闢不成功 { printf ("\nCann't create it, try it again in a moment!\n"); return NULL; } else //節點開闢成功 { head = NULL; //開始head指向NULL printf ("Please input %d node -- num,score: ", n + 1); scanf ("%d %f", &(p1->num), &(p1->score)); //錄入資料 } while(p1->num != 0) //只要學號不為0,就繼續錄入下一個節點 { n += 1; //節點總數增加1個 if(n == 1) //如果節點總數是1,則head指向剛建立的節點p1 { head = p1; p2->next = NULL; //此時的p2就是p1,也就是p1->next指向NULL。 } else { p2->next = p1; //指向上次下面剛剛開闢的新節點 } p2 = p1; //把p1的地址給p2保留,然後p1產生新的節點 p1 = (struct student *) malloc (LEN); printf ("Please input %d node -- num,score: ", n + 1); scanf ("%d %f", &(p1->num), &(p1->score)); } p2->next = NULL; //此句就是根據單向連結串列的最後一個節點要指向NULL free(p1); //p1->num為0的時候跳出了while迴圈,並且釋放p1 p1 = NULL; //特別不要忘記把釋放的變數清空置為NULL,否則就變成"野指標",即地址不確定的指標 return head; //返回建立連結串列的頭指標 } /* =========================== 功能:輸出節點 返回: void =========================== */ void Print(struct student *head) { struct student *p; printf ("\nNow , These %d records are:\n", n); p = head; if(head != NULL) //只要不是空連結串列,就輸出連結串列中所有節點 { printf("head is %o\n", head); //輸出頭指標指向的地址 do { /* 輸出相應的值:當前節點地址、各欄位值、當前節點的下一節點地址。 這樣輸出便於讀者形象看到一個單向連結串列在計算機中的儲存結構,和我們 設計的圖示是一模一樣的。 */ printf ("%o %d %5.1f %o\n", p, p->num, p->score, p->next); p = p->next; //移到下一個節點 } while (p != NULL); } } /* ========================== 功能:刪除指定節點 (此例中是刪除指定學號的節點) 返回:指向連結串列表頭的指標 ========================== */ struct student *Del (struct student *head, int num) { struct student *p1; //p1儲存當前需要檢查的節點的地址 struct student *p2; //p2儲存當前檢查過的節點的地址 if (head == NULL) //是空連結串列(結合圖3理解) { printf ("\nList is null!\n"); return head; } //定位要刪除的節點 p1 = head; while (p1->num != num && p1->next != NULL) //p1指向的節點不是所要查詢的,並且它不是最後一個節點,就繼續往下找 { p2 = p1; //儲存當前節點的地址 p1 = p1->next; //後移一個節點 } if(p1->num==num) //找到了。(結合圖4、5理解) { if (p1 == head) //如果要刪除的節點是第一個節點 { head = p1->next; //頭指標指向第一個節點的後一個節點,也就是第二個節點。這樣第一個節點就不在連結串列中,即刪除 } else //如果是其它節點,則讓原來指向當前節點的指標,指向它的下一個節點,完成刪除 { p2->next = p1->next; } free (p1); //釋放當前節點 p1 = NULL; printf ("\ndelete %ld success!\n", num); n -= 1; //節點總數減1個 } else //沒有找到 { printf ("\n%ld not been found!\n", num); } return head; } //銷燬連結串列 void DestroyList(struct student *head) { struct student *p; if(head==NULL) return 0; while(head) { p=head->next; free(head); head=p; } return 1; } /* ========================== 功能:插入指定節點的後面 (此例中是指定學號的節點) 返回:指向連結串列表頭的指標 ========================== */ struct student *Insert (struct student *head, int num, struct student *node) { struct student *p1; //p1儲存當前需要檢查的節點的地址 if (head == NULL) //(結合圖示7理解) { head = node; node->next = NULL; n += 1; return head; } p1 = head; while(p1->num != num && p1->next != NULL) //p1指向的節點不是所要查詢的,並且它不是最後一個節點,繼續往下找 { p1 = p1->next; //後移一個節點 } if (p1->num==num) //找到了(結合圖示8理解) { node->next = p1->next; //顯然node的下一節點是原p1的next p1->next = node; //插入後,原p1的下一節點就是要插入的node n += 1; //節點總數增加1個 } else { printf ("\n%ld not been found!\n", num); } return head; } /* ========================== 功能:反序節點 (連結串列的頭變成連結串列的尾,連結串列的尾變成頭) 返回:指向連結串列表頭的指標 ========================== */ struct student *Reverse (struct student *head) { struct student *p; //臨時儲存 struct student *p1; //儲存返回結果 struct student *p2; //源結果節點一個一個取 p1 = NULL; //開始顛倒時,已顛倒的部分為空 p2 = head; //p2指向連結串列的頭節點 while(p2 != NULL) { p = p2->next; p2->next = p1; p1 = p2; p2 = p; } head = p1; return head; } /* ========================== 功能:選擇排序(由小到大) 返回:指向連結串列表頭的指標 ========================== */ struct student *SelectSort (struct student *head) { struct student *first; //排列後有序鏈的表頭指標 struct student *tail; //排列後有序鏈的表尾指標 struct student *p_min; //保留鍵值更小的節點的前驅節點的指標 struct student *min; //儲存最小節點 struct student *p; //當前比較的節點 first = NULL; while(head != NULL) //在連結串列中找鍵值最小的節點 { //注意:這裡for語句就是體現選擇排序思想的地方 for (p = head, min = head; p->next != NULL; p = p->next) //迴圈遍歷連結串列中的節點,找出此時最小的節點 { if (p->next->num < min->num) //找到一個比當前min小的節點 { p_min = p; //儲存找到節點的前驅節點:顯然p->next的前驅節點是p min = p->next; //儲存鍵值更小的節點 } } //上面for語句結束後,就要做兩件事;一是把它放入有序連結串列中;二是根據相應的條件判斷,安排它離開原來的連結串列 //第一件事 if (first == NULL) //如果有序連結串列目前還是一個空連結串列 { first = min; //第一次找到鍵值最小的節點 tail = min; //注意:尾指標讓它指向最後的一個節點 } else //有序連結串列中已經有節點 { tail->next = min; //把剛找到的最小節點放到最後,即讓尾指標的next指向它 tail = min; //尾指標也要指向它 } //第二件事 if (min == head) //如果找到的最小節點就是第一個節點 { head = head->next; //顯然讓head指向原head->next,即第二個節點,就OK } else //如果不是第一個節點 { p_min->next = min->next; //前次最小節點的next指向當前min的next,這樣就讓min離開了原連結串列 } } if (first != NULL) //迴圈結束得到有序連結串列first { tail->next = NULL; //單向連結串列的最後一個節點的next應該指向NULL } head = first; return head; } /* ========================== 功能:直接插入排序(由小到大) 返回:指向連結串列表頭的指標 ========================== */ struct student *InsertSort (struct student *head) { struct student *first; //為原連結串列剩下用於直接插入排序的節點頭指標 struct student *t; //臨時指標變數:插入節點 struct student *p,*q; //臨時指標變數 first = head->next; //原連結串列剩下用於直接插入排序的節點連結串列:可根據圖12來理解 head->next = NULL; //只含有一個節點的連結串列的有序連結串列:可根據圖11來理解 while(first != NULL) //遍歷剩下無序的連結串列 { //注意:這裡for語句就是體現直接插入排序思想的地方 for (t = first, q = head; ((q != NULL) && (q->num < t->num)); p = q, q = q->next); //無序節點在有序連結串列中找插入的位置 //退出for迴圈,就是找到了插入的位置,應該將t節點插入到p節點之後,q節點之前 //注意:按道理來說,這句話可以放到下面註釋了的那個位置也應該對的,但是就是不能。原因:你若理解了上面的第3條,就知道了 //下面的插入就是將t節點即是first節點插入到p節點之後,已經改變了first節點,所以first節點應該在被修改之前往後移動,不能放到下面註釋的位置上去 first = first->next; //無序連結串列中的節點離開,以便它插入到有序連結串列中 if (q == head) //插在第一個節點之前 { head = t; } else //p是q的前驅 { p->next = t; } t->next = q; //完成插入動作 //first = first->next; } return head; } /* ========================== 功能:氣泡排序(由小到大) 返回:指向連結串列表頭的指標 ========================== */ struct student *BubbleSort (struct student *head) { struct student *endpt; //控制迴圈比較 struct student *p; //臨時指標變數 struct student *p1,*p2; p1 = (struct student *) malloc (LEN); p1->next = head; //注意理解:我們增加一個節點,放在第一個節點的前面,主要是為了便於比較。因為第一個節點沒有前驅,我們不能交換地址 head = p1; //讓head指向p1節點,排序完成後,我們再把p1節點釋放掉 for (endpt = NULL; endpt != head; endpt = p) //結合第6點理解 { for (p = p1 = head; p1->next->next != endpt; p1 = p1->next) { if (p1->next->num > p1->next->next->num) //如果前面的節點鍵值比後面節點的鍵值大,則交換 { p2 = p1->next->next; //結合第1點理解 p1->next->next = p2->next; //結合第2點理解 p2->next = p1->next; //結合第3點理解 p1->next = p2; //結合第4點理解 p = p1->next->next; //結合第6點理解 } } } p1 = head; //把p1的資訊去掉 head = head->n