1. 程式人生 > >三、連結串列(實踐)

三、連結串列(實踐)

絮絮叨叨

如何輕鬆寫連結串列的程式碼?

  • 有決心並付出精力
  • 理解指標或引用的含義
    • 將某個變數賦值給指標,實際上就是將這個變數的地址賦值給指標,或者反過來說,指標中儲存了這個變數的記憶體地址,指向了這個變數,通過指標就能找到這個變數。
  • 警惕指標丟失和記憶體洩漏
  • 利用哨兵(頭結點)簡化實現難度
  • 重點留意邊界條件處理
    • 如果連結串列為空時,程式碼是否能正常工作?
    • 如果連結串列只包含一個結點時,程式碼是否能正常工作?
    • 如果連結串列只包含兩個結點時,程式碼是否能正常工作?
    • 程式碼邏輯在處理頭結點和尾結點的時候,是否能正常工作?
  • 舉例畫圖,輔助思考

一、資料結構

1、單向連結串列

  • 結點包括:資料域 + 指標域
    • 資料域:儲存資料元素的值
    • 指標域(鏈域):儲存下一個結點地址或者指向其後繼結點的指標
// 定義結點 Node
typedef struct Node{
	ElemType data;
	struct Node * next;
} Node;
// 定義指向結點 Node 型別物件的指標 LinkList
typedef struct Node *LinkList; 

2、雙向連結串列

  • 結點包括:資料域 + 左指標域(prev) + 右指標域(next)
struct DNode{
	int data;
	DNode * prev;
	DNode * next;
}

二、基本操作例項

1、單鏈表的讀取

(1)獲取連結串列第 i 個數據結點的演算法思路:

  • 宣告一個指標 p 指向連結串列的第一個結點,初始化 j 從 1 開始;
  • 當 j < i 時,遍歷連結串列,p 不斷指向下一個結點, j++;
  • 若到連結串列末尾 p 為空, 則說明第 i 個元素不存在;
  • 否則查詢成功,返回結點 p 的資料。

(2)實現

由於單鏈表的結構中沒有定義表長,所以不能事先知道要迴圈多少次,因此不方便用for迴圈來控制迴圈。==》while迴圈

# define OK 1
# define ERROR 0
# define TRUE 1
# define FALSE 0
typdef int Status; //Status是函式的型別,其值為函式結果狀態碼,eg:OK等
/*初始條件:順序線性表L已存在,1≤i≤ListLength(L)*/
/*操作結果: 用e返回L中第i個數據元素的值*/
Status GetElem( Node *L, int i, Elemtype *e){	
	int j;
	LinkList p; // 宣告指標p
	p = L->next; //讓p指向連結串列L的第一個節點
	j = 1;
	while(p && j<i) //當p不為空 或 j 小於i時,繼續迴圈
	{
		p = p->next;
		++j;
	}
	if(!p || j>i )
		return ERROR;
	*e = p->data;
	return OK;
}

2、插入結點(單向連結串列)

s->next = p->next;
p->next = s

在這裡插入圖片描述

(1)第 i 個數據插入結點的演算法思路:

  • 宣告一個指標p指向連結串列的第一個結點,初始化j=1;
  • 當 j < i 時,遍歷連結串列,讓指標 p 向後移動,不斷指向下一結點,++j;
  • 若到連結串列末尾 p 為空,說明第 i 個元素不存在;
  • 否則查詢成功,在系統中生成一個空結點 s;
  • 將資料元素 e 賦值給 s->data;
  • 單鏈表的插入標準語句:s->next = p->next; p->next = s
  • 返回成功

(2)實現

/*初始條件:順序線性表L已存在,1≤i≤ListLength(L)*/
/*操作結果:在L中第i個位置之前插入新的資料元素e,L的長度加1*/
Status ListInsert(LinkList *L, int i, ElemType e)
{
	int j;
	LinkList p,s;
	p = *L;
	j = 1;
	while(p && j<i)  //尋找第i個結點
	{
		p = p->next;
		++j;
	}
	if(!p || j>i ) // 第i個結點不存在
		return ERROR;
	s = (LinkList)malloc(sizeof(Node)); //生成新的結點
	s->data = e;
	s->next = p->next; //將p的後繼結點賦值給s的後繼
	p->next = s;       //將s賦值給p的後繼
	return OK;
}

3、刪除結點(單向連結串列)

p->next = p->next->next
用q取代p->next的話,上面等價於:

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

在這裡插入圖片描述

(1)第 i 個數據刪除結點的演算法思路:

  • 宣告一個指標p指向連結串列的第一個結點,初始化 j 從1開始;
  • 當 j<i 時,遍歷連結串列,讓指標p向後移動,不斷,指向下一結點,++j;
  • 若到連結串列末尾p為空,說明第i個結點不存在;
  • 否則查詢成功,將欲刪除的結點p->next賦值給q;
  • 單鏈表的刪除標準語句:p->next = q->next;
  • 將q結點中的資料賦值給e,作為函式的返回值
  • 釋放q結點
  • 返回成功

(2)實現

#include<stdio.h>

Status ListDelete(LinkList *L, int i, ElemType *e)
{
	int j;
	LinkList p,q;
	p = *L;
	j = 1;
	while(p->next && j<i){   //遍歷查詢第i個結點
		p = p->next;
		++j;
	}
	if(!(p->next) || j>i)
		return ERROR;
	q = p->next;
	p->next = q->next;
	*e = q->data;
	free(q);
	return OK;
}

4、單鏈表的整表建立

單鏈表的整表建立過程就是一個動態生成連結串列的過程。由“空表”的初始狀態,依次建立各元素結點,並逐個插入連結串列。

(1)演算法思路(頭插法):

  • 宣告一結點 p 和 計數器變數 i;
  • 初始化一空連結串列 L;
  • 讓 L 的頭結點的指標指向NULL,即建立一個帶頭結點的單鏈表;
  • 迴圈:
    • 生成一個新結點賦值給 p;
    • 隨機生成一個數組賦值給 p 的資料域 p->data;
    • 將 p 插入到頭結點與前一新結點之間。

(2)實現

頭插法

/* 隨機產生n個元素的值,建立帶頭結點的單鏈表L */ 
void CreateListHead(LinkList *L, int n)
{	
	LinkList p;
	int i;
	srand(time(0));     //初始化隨機種子
	*L = (LinkList)malloc(sizeof(Node));
	(*L)->next = NULL;  //建立一個帶頭結點的單鏈表
	for(i = 0; i < n; i++)
	{
		p = (LinkList)malloc(sizeof(Node)); //生成新結點
		p->data = rand()%100 + 1;
		p->next = (*L)->next;
		(*L)->next = p;
	}
}

尾插法

/* 隨機產生n個元素的值,建立帶頭結點的單鏈表L */ 
void CreateListHead(LinkList *L, int n)
{ 
	LinkList p, r;
	int i;
 	srand(time(0));     //初始化隨機種子
 	*L = (LinkList)malloc(sizeof(Node));
 	r = *L;             // *r 指向尾部的結點
 	for(i = 0; i < n; i++)
 	{
  		p = (LinkList)malloc(sizeof(Node)); //生成新結點
  		p->data = rand()%100 + 1;
  		r->next = p;
 		r = p;
 	}
 	r->next=NULL;
}

5、單鏈表的整表刪除

(1)演算法思路

  • 宣告結點結點 p 和 q;
  • 將第一個結點賦值給 p;
  • 迴圈:
    • 將下一結點賦值給 q;
    • 釋放 p;
    • 將 q 賦值給 p;

(2)實現

/*初始條件:順序線性表L已存在,操作結果:將L充值為空表*/
Status CLearList(LinkList *L)
{	
	LinkList p,q;
	p = (*L)->next;
	while(p)
	{
		q = p->next;
		free(p);
		p = q;
	}
	(*L)->next = NULL;
	return OK;
}

三、常見操作

1、單鏈表反轉

法一:反向遍歷連結串列就類似於事先遍歷的節點後輸出,即“先進後出”,那麼可以將連結串列遍歷存放於棧中,其後遍歷棧依次彈出棧節點,達到反向遍歷效果。

//1.stack
void printLinkedListReversinglyByStack(Node *head){
    stack<Node* > nodesStack;
    Node* pNode = head;
    //遍歷連結串列
    while (pNode != NULL) {
        nodesStack.push(pNode);
        pNode = pNode->next;
    }
    while (!nodesStack.empty()) {
        pNode=nodesStack.top();
        printf("%d\t", pNode->value);
        nodesStack.pop();
    }
}
//2.遞迴
void printLinkedListReversinglyRecursively(Node *head){
    if (head!=NULL) {
        if (head->next!=NULL) {
            printLinkedListReversinglyRecursively(head->next);
        }
        printf("%d\t", head->value);
    }
}

2、連結串列中環的檢查,獲取連線點,計算環的長度

判斷連結串列是否有環路,獲取連線點,計算環的長度
此題很有意思,具體詳細請參考:http://www.cnblogs.com/xudong-bupt/p/3667729.html

判斷是否含有環:slow和fast,slow指標每次走一步,fast指標每次走兩步,若是連結串列有環,fast必能追上slow(相撞),若fast走到NULL,則不含有環。

//判斷是否含有環
bool containLoop(Node* head){
    if (head==NULL) {
        return false;
    }
    Node* slow = head;
    Node* fast = head;
    while (slow!=fast&&fast->next!=NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    if (fast==NULL) {
        return false;
    }
    return true;
}

判斷環的長度:在相撞點處,slow和fast繼續走,當再次相撞時,slow走了length步,fast走了2*length步,length即為環得長度。

//獲得環的長度
int getLoopLength(Node* head){
    if (head==NULL) {
        return 0;
    }
    Node* slow = head;
    Node* fast = head;
    while (slow!=fast&&fast->next!=NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    if (fast==NULL) {
        return 0;
    }
    //slow和fast首次相遇後,slow和fast繼續走
    //再次相遇時,即slow走了一圈,fast走了兩圈
    int length = 0;
    while (slow!=fast) {
        length++;
        slow = slow->next;
        fast = fast->next->next;
    }
    return length;
}

環得連線點:slow和fast第一次碰撞點到環的連線點的距離=頭指標到環的連線點的距離,此式可以證明,詳見上面連結。

//獲得環的連線點
Node* getJoinpoit(Node* head){
    if (head==NULL) {
        return NULL;
    }
    Node* slow = head;
    Node* fast = head;
    while (slow!=fast&&fast->next!=NULL) {
        slow = slow->next;
        fast = fast->next->next;
    }
    if (fast==NULL) {
        return NULL;
    }
    Node* fromhead = head;
    Node* fromcrashpoint = slow;

    while (fromcrashpoint!=fromhead) {
        fromhead = fromhead->next;
        fromcrashpoint = fromcrashpoint->next;
    }
    return fromhead;
}

3、找出中間節點

用slow和fast指標標記,slow每次走一步,fast每次走兩步,當fast到尾節點時,slow就相當於總長度的一半,即在中間節點。

//找出中間節點
Node* findMidNode(Node* head){
    Node* slow = head;
    Node* fast = head;
    while (fast->next != 0&&fast->next->next!=0) {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

4、找出倒數第k個節點

用slow和fast指標標記,fast指標事先走k步,然後slow和fast同時走,當fast到達末節點時,slow在fast的前k個節點,即為倒數第k個節點。

//找出倒數第k個節點
Node* findKNode(Node* head,int k){
    Node *temp1 = head;
    Node *temp2 = head;
    while (k-->0) {
        if(temp2 == NULL){
            return NULL;
        }
        temp2 =temp2->next;
    }
    while (temp2->next != NULL&&temp2->next->next!=NULL) {
        temp1 = temp1->next;
        temp2 = temp2->next->next;
    }
    return temp1;
}