數據結構和算法(二)單向循環鏈表的創建插入刪除實現

上一篇博客“線性表” 詳細講解了順序表和單鏈表的基本操作。本篇博客主要講解對於循環鏈表的基本操作。

1. 線性表概要

我們先來總的看一下線性表主要有哪些操作,如下圖:

線性表的主要操作

紅框裏面的內容是本篇博客主要講解的內容,後面的博客會繼續講解雙向鏈表,循環雙向鏈表等。

先回顧一下單鏈表

鏈表是一種線性表, 也是一種存儲數據的數據結構.如下圖:這種的一個節點中包含自身數據以及指向下一個節點的位置,一個嵌套着下一個. 這中結構就稱之爲鏈表.
鏈表結構
鏈表

2. 單向循環鏈表

本篇博客demo點擊這裏下載:單向循環鏈表demo

單向循環鏈表結點的定義跟單鏈表一樣,區別就是尾結點執行頭結點,形成一個環。 如下圖(1)表示一個單向循環鏈表,由尾結點指向頭結點:
圖1:單向循環鏈表

我們再來回顧一下單向鏈表的結構圖:如下圖:圖2

圖2:單向鏈表

我們對比看,可以明顯的看出,單向循環鏈表和單向鏈表的區別,僅僅是多了一個尾結點指針指向頭結點的過程,他們的結點定義是一樣的。

  • 單向循環鏈表結點定義如下:
typedef int KStatus;
typedef int KElementType;
typedef struct KNode {
    KElementType data;//用來存儲一個int數據(具體數據類型根據開發實際情況而定,此處使用int)
    struct KNode *next;//指向下一個節點的指針
} Node;
typedef struct KNode * LinkList;

2.1 建表

單向循環鏈表的建表過程跟單鏈表非常相似。我們一般都採用尾部插入法,這樣比較符合我們的業務邏輯,尾部插入法的得到的數據和我們定義的順序是一樣的,如果採用頭部插入法,則得到的是一個逆序的表。

如我們初始化鏈表時,只是有一個頭結點,它的next指針指向它自己,如下圖3是一個空鏈表:
圖3:空鏈表

我們採用尾部插入法,分別插入數據9,16,18,20 就得到了下面的鏈表,如圖4:

圖4:非空單向循環鏈表

我們插入建表的過程如下:

分爲兩種情況:① 第一次開始創建; ②已經創建,往裏面新增數據

判斷是否第一次創建鏈表:

YES->創建一個新結點,並使得新結點的next 指向自身; (*L)->next = (*L);
NO-> 找鏈表尾結點,將尾結點的next = 新結點. 新結點的next = (*L);

我們細分一下過程如下:

  1. 我們先初始化得到一個空鏈表如下:
    圖3:空鏈表
  2. 插入1個結點後如下:

第二步:插入一個結點
3. 插入2個結點後如下:
第三步:插入2個結點

代碼實現如下:

  • 方式1:
//1. 創建循環鏈表
//2種情況:① 第一次開始創建; ②已經創建,往裏面新增數據
/* 1. 判斷是否第一次創建鏈表
   YES->創建一個新結點,並使得新結點的next 指向自身; (*L)->next = (*L);
   NO-> 找鏈表尾結點,將尾結點的next = 新結點. 新結點的next = (*L);
 */
KStatus createCircleList(LinkList *L) {

    int item;
    LinkList temp = NULL;
    LinkList target = NULL;

    printf("輸入節點的值,輸入0結束\n");
    while (1) {
        scanf("%d",&item);

        if(item == 0) break;

        //判斷,如果輸入的鏈表是空,則創建一個新結點,並且使其next指針指向自己  (*head)->next=*head;
        if(NULL == *L) {
            *L = (LinkList)malloc(sizeof(Node));
            if(!L) return ERROR;
            (*L)->data = item;
            (*L)->next = *L;
        } else {
            //如果輸入的鏈表不爲空,則尋找鏈表的尾結點,是尾結點的next=新節點。新節點的next指向頭節點
            for (target = *L; target->next != *L; target = target->next);

            temp = (LinkList)malloc(sizeof(Node));
            if(!temp) return ERROR;

            //結點賦值,並插入尾部
            temp->data = item;
            //1.新節點指向頭節點
            temp->next = *L;
            //2.尾節點指向新節點
            target->next = temp;

        }
    }

    return OK;
}

上面的代碼我們通過一個for循環來查找到鏈表的尾部結點,實際上我們初始化鏈表的時候,可以用一個指針r專門指向尾結點,這樣我們就沒有必要循環遍歷去查找尾結點了,修改後的代碼如下:

  • 方式2:
KStatus createCircleList2(LinkList *L) {

    int item;
    LinkList temp = NULL;
    //LinkList target = NULL;
    //增加一個賦值指針,指向尾結點
    LinkList r = *L;

    printf("輸入節點的值,輸入0結束\n");
    while (1) {
        scanf("%d",&item);

        if(item == 0) break;

        //判斷,如果輸入的鏈表是空,則創建一個新結點,並且使其next指針指向自己  (*head)->next=*head;
        if(NULL == *L) {
            *L = (LinkList)malloc(sizeof(Node));
            if(!L) return ERROR;
            (*L)->data = item;
            (*L)->next = *L;
            //記錄尾結點
            r = *L;
        } else {
            //如果輸入的鏈表不爲空,則尋找鏈表的尾結點,是尾結點的next=新節點。新節點的next指向頭節點
            //for (target = *L; target->next != *L; target = target->next);
            //這裏已經用r指向了尾結點,不需要查找了。

            temp = (LinkList)malloc(sizeof(Node));
            if(!temp) return ERROR;

            //結點賦值,並插入尾部
            temp->data = item;
            //1.新節點指向頭節點
            temp->next = *L;
            //2.尾節點指向新節點
            r->next = temp;

            //移動r指針,保存r指針一直指向尾結點
            r = temp;

        }
    }

    return OK;
}

2.2 遍歷

循環鏈表的遍歷很簡單,一個do while 語句就做完了,如下遍歷代碼:

// 2.遍歷循環鏈表
void traverseCircleList(LinkList L) {
    //判斷鏈表是否爲空
    if(!L) {
        printf("鏈表爲空!\n");
        return;
    }

    LinkList temp;
    temp = L;
    do {
        printf("%6d", temp->data);
        temp = temp->next;
    }while (temp != L) ;
    printf("\n");
}

2.3 插入

單向循環鏈表的插入也跟單向鏈表插入相似,只是多了尾部結點指針特殊處理。

單向循環鏈表的插入又分爲兩種情況:

  • 第一種情況:插入位置在首結點,需要單獨特殊處理
  • 第二種情況: 其他位置(非首結點)可以統一處理

爲了更好的理解,下面我們通過圖來區分兩種情況的插入。

第一種情況:插入位置在首結點,如下圖(圖2.3.1):
圖2.3.1:情況1 插入位置在首結點

根據上圖,我們來分析一下第一種情況的插入過程大致如下:
首先我們創建一個新結點,賦值爲2,
然後,我們用下面三步完成結點2的插入
1、將新創建的結點 next 指向原來的首元結點。(上圖對應標號2,我們用結點X的next指針指向結點A)
2、將尾結點 next 指向新創建的結點。(上圖對應標號3,將尾結點C的next指針指向新的頭結點X)
3、將L指針指向新創建的結點。(上圖對應標號4 ,將L指向新的首結點X)

完成插入後得到:

圖2.3.2:情況1 插入位置在首結點,插入完成

注意:這裏步驟1,2的順序不能更換,假設我們先指向步驟2,再執行步驟1,那麼我們原來的首節點1,就會丟失,找不到了。我們鏈表插入的最重要的原則就是,保證鏈表不要丟失結點。斷開結點前必須有指針指向即將要斷開的結點,這樣才能保證不丟失結點。

第二種情況: 其他位置(非首結點),如下圖:

圖2.3.3:情況3 插入位置在非首結點

如上圖表示的第二種情況,在非首節點插入新創建的結點X
如上圖所示,我們同樣先新建一個結點X,假設我們要插入在B和C之間,那麼我們首先需要找到C結點的前一個結點B,然後再用同樣的3步完成插入操作。
插入過程:
1、找到插入結點前一個結點(B結點),(我們用target指針指向B.)
2、將新創建結點X的next 指向插入位置後面的結點(X->next = C)
3、然後再把插入位置前面的結點next 指向新創建的結點(B->next= X)

完成插入後我們得到如下:

圖2.3.3:情況3 插入位置在非首結點,插入完成

插入結點的實現代碼如下:

// 3. 循環鏈表插入元素
// L : 鏈表指針
// place: 要插入的位置
// data : 要插入的數據
KStatus insertElement(LinkList *L, int place, int data) {

    LinkList temp, target;
    int i;

    //先判斷位置是否爲首結點,在首結點需要單獨處理
    if (place == 1) {
        //1. 創建一個新結點
        temp = (LinkList)malloc(sizeof(Node));
        //判斷內存是否分配成功
        if (! temp) return ERROR;
        //賦值
        temp->data = data;

        //2. 找到鏈表的最後一個結點,尾結點
        for(target = *L; target->next != *L ; target = target->next);

        //3.找到尾結點,插入到尾結點後面
        //3.1 讓新結點的next,指向新的頭結點
        temp->next = *L;
        //3.2 讓尾結點的next指向新的結點,這樣新的結點成了新的尾結點。
        target->next = temp;
        //3.3 讓頭指針指向新的尾結點
        *L = temp;

    } else { //其他位置
        //1. 創建一個新結點
        temp = (LinkList)malloc(sizeof(Node));
        //判斷內存是否分配成功
        if (! temp) return ERROR;
        //賦值
        temp->data = data;

        //2. 找到鏈表的第place - 1 的位置
        for(i = 1, target = *L; target->next != *L && i != place - 1 ; target = target->next, i++);

        //3. 指向插入操作,將新結點插入到place位置
        //3.1 新結點的next 指向target原來的next位置 ;
        temp->next = target->next;
        //3.2 插入結點的前驅指向新結點
        target->next = temp;
    }
    return OK;
}

2.4 刪除

刪除跟插入一樣,也是分爲兩種情況:

  • 第一種情況:刪除首結點,需要特殊處理。
  • 第二種情況:刪除其他結點,非首結點,可以統一處理。

對於第一種情況(刪除首結點):

圖2.4.1:刪除首結點

我們來分析一下過程:
如上圖,我們要刪除首結點A,我們先要找到尾結點C。
然後,將尾結點C的next,指向新的首結點B。(C->next = B)
然後,將L指向新的首結點B。(*L = B)
最後釋放A結點的內存。

第二種情況刪除非首結點:

圖2.4.2:刪除非首結點

我們刪除非首結點的話就可以統一處理,如上圖,假設我們刪除結點X,
那我們先要找到X的上一個結點B。
然後,將X的上一個結點也就是B的next執行 X的下一個結點也就是C。 (B->next = C 或者 B->next = B->next->next)
最後我們釋放X結點的內存。

實現代碼如下:

// 4. 循環鏈表刪除元素
// 刪除第place位置的元素
// L : 鏈表指針
// place: 要刪除的位置
KStatus deleteElement(LinkList *L, int place) {

    LinkList temp, target;
    int i;
    //temp 指向鏈表首元結點
    temp = *L;
    //如果爲空鏈表,則結束
    if (!temp) return ERROR;

    //判斷是否是首節點,如果是首結點需要單獨特殊處理
    if (place == 1) {
         //①.如果刪除到只剩下首元結點了,則直接將*L置空;
        if((*L)->next == (*L)) {
            *L = NULL;
            return OK;
        }

        //②.鏈表還有很多數據,但是刪除的是首結點;
        // 2.1 先要找到尾結點,
        for (target = *L; target->next != *L; target = target->next) ;
        // 2.2 使得尾結點next 指向頭結點的下一個結點 target->next = (*L)->next;
        temp = *L;
        *L = (*L)->next;
        // 2.3 新結點做爲頭結點,則釋放原來的頭結點
        target->next = *L;
        //釋放資源
        free(temp);
    } else { //其他位置,統一處理
        //遍歷找到第place-1個位置
        for (i = 1, target = *L; target->next != *L && i != place - 1; target = target->next, i++) {
            //找到刪除結點前一個結點target
            //用temp指向要刪除的元素,第place位置
            temp = target->next;
            //使得target->next 指向下一個結點
            target->next = temp->next;

            //釋放內存
            free(temp);
        }
    }
    return OK;
}

2.5 查詢

查詢非常結點,一個while循環就弄完了,實現代碼如下:

//5. 循環列表查詢
// 返回位置的索引值
int queryValue(LinkList L , int value) {
    int i = 1;
    LinkList p = L;

    //尋找鏈表中的結點 data == value
    while (p->data != value) {
        i++;
        p = p->next;
    }
    //當尾結點指向頭結點就會直接跳出循環,所以要額外增加一次判斷尾結點的data == value;
    if (p->next == L && p->data != value) return -1; //沒有找到
    //找到了元素,返回下標
    return i;
}

2.6 單元測試

// 6. 單元測試
void test() {
       LinkList head;
        int place,num;
        int iStatus;

        iStatus = createCircleList(&head);
        //iStatus = createCircleList2(&head);
        printf("原始的鏈表:\n");
        traverseCircleList(head);

    //    printf("輸入要插入的位置和數據用空格隔開:");
    //    scanf("%d %d",&place,&num);
    //    iStatus = insertElement(&head,place,num);
    //    traverseCircleList(head);

        printf("輸入要刪除的位置:");
        scanf("%d",&place);
        deleteElement(&head,place);
        traverseCircleList(head);


        printf("輸入你想查找的值:");
        scanf("%d",&num);
        place = queryValue(head,num);
        if(place!=-1)
            printf("找到的值的位置是place = %d\n",place);
        else
            printf("沒找到值\n");
}

2.7 完整代碼實現

//
//  main.c
//  004_CircularLinedList
//
//  Created by 孔雨露 on 2020/4/5.
//  Copyright © 2020 Apple. All rights reserved.
//

#include <stdio.h>
#include "string.h"
#include "ctype.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"

#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OK 1

#define MAXSIZE 20 /* 存儲空間初始分配量 */

typedef int Status;/* Status是函數的類型,其值是函數結果狀態代碼,如OK等 */
typedef int ElemType;/* ElemType類型根據實際情況而定,這裏假設爲int */

//定義結點
typedef struct Node{
    ElemType data;
    struct Node *next;
}Node;

typedef struct Node * LinkList;

/*
 4.1 循環鏈表創建!
 2種情況:① 第一次開始創建; ②已經創建,往裏面新增數據
 
 1. 判斷是否第一次創建鏈表
    YES->創建一個新結點,並使得新結點的next 指向自身; (*L)->next = (*L);
    NO-> 找鏈表尾結點,將尾結點的next = 新結點. 新結點的next = (*L);
 */
Status CreateList(LinkList *L){

    int item;
    LinkList temp = NULL;
    LinkList target = NULL;
    printf("輸入節點的值,輸入0結束\n");
    while(1)
    {
        scanf("%d",&item);
        if(item==0) break;
        
          //如果輸入的鏈表是空。則創建一個新的節點,使其next指針指向自己  (*head)->next=*head;
        if(*L==NULL)
        {
            *L = (LinkList)malloc(sizeof(Node));
            if(!L)exit(0);
            (*L)->data=item;
            (*L)->next=*L;
        }
        else
        {
           //輸入的鏈表不是空的,尋找鏈表的尾節點,使尾節點的next=新節點。新節點的next指向頭節點
            
            for (target = *L; target->next != *L; target = target->next);
            
            temp=(LinkList)malloc(sizeof(Node));
            
            if(!temp) return ERROR;
            
            temp->data=item;
            temp->next=*L;  //新節點指向頭節點
            target->next=temp;//尾節點指向新節點
        }
    }
    
    return OK;
}

Status CreateList2(LinkList *L){
    
    int item;
    LinkList temp = NULL;
    LinkList target = NULL;
    LinkList r = NULL;
    printf("請輸入新的結點, 當輸入0時結束!\n");
    while (1) {
        scanf("%d",&item);
        if (item == 0) {
            break;
        }
        
        //第一次創建
        if(*L == NULL){
            
            *L = (LinkList)malloc(sizeof(Node));
            if(!*L) return ERROR;
            (*L)->data = item;
            (*L)->next = *L;
            r = *L;
        }else{
            
            temp = (LinkList)malloc(sizeof(Node));
            if(!temp) return  ERROR;
            temp->data = item;
            temp->next = *L;
            r->next = temp;
            
            r = temp;
        }
        
    }
    
    return OK;
}



//4.2 遍歷循環鏈表,循環鏈表的遍歷最好用do while語句,因爲頭節點就有值
void show(LinkList p)
{
    //如果鏈表是空
    if(p == NULL){
        printf("打印的鏈表爲空!\n");
        return;
        
    }else{
        LinkList temp;
        temp = p;
        do{
            printf("%5d",temp->data);
            temp = temp->next;
        }while (temp != p);
        printf("\n");
    }
    
}

//4.3 循環鏈表插入數據
Status ListInsert(LinkList *L, int place, int num){
    
    LinkList temp ,target;
    int i;
    if (place == 1) {
        
        //如果插入的位置爲1,則屬於插入首元結點,所以需要特殊處理
        //1. 創建新結點temp,並判斷是否創建成功,成功則賦值,否則返回ERROR;
        //2. 找到鏈表最後的結點_尾結點,
        //3. 讓新結點的next 執行頭結點.
        //4. 尾結點的next 指向新的頭結點;
        //5. 讓頭指針指向temp(臨時的新結點)
        
        temp = (LinkList)malloc(sizeof(Node));
        if (temp == NULL) {
            return ERROR;
        }
        temp->data = num;
        
        for (target = *L; target->next != *L; target = target->next);
        
        temp->next = *L;
        target->next = temp;
        *L = temp;
        
    }else
    {
        
        //如果插入的位置在其他位置;
        //1. 創建新結點temp,並判斷是否創建成功,成功則賦值,否則返回ERROR;
        //2. 先找到插入的位置,如果超過鏈表長度,則自動插入隊尾;
        //3. 通過target找到要插入位置的前一個結點, 讓target->next = temp;
        //4. 插入結點的前驅指向新結點,新結點的next 指向target原來的next位置 ;
        
        temp = (LinkList)malloc(sizeof(Node));
        if (temp == NULL) {
            return ERROR;
        }
        temp->data = num;
        
        for ( i = 1,target = *L; target->next != *L && i != place - 1; target = target->next,i++) ;
        
        temp->next = target->next;
        target->next = temp;
    }
    
    return OK;
}

//4.4 循環鏈表刪除元素
Status  LinkListDelete(LinkList *L,int place){
    
    LinkList temp,target;
    int i;
    //temp 指向鏈表首元結點
    temp = *L;
    if(temp == NULL) return ERROR;
    
    
    if (place == 1) {
        
        //①.如果刪除到只剩下首元結點了,則直接將*L置空;
        if((*L)->next == (*L)){
            (*L) = NULL;
            return OK;
        }
        
        
        //②.鏈表還有很多數據,但是刪除的是首結點;
        //1. 找到尾結點, 使得尾結點next 指向頭結點的下一個結點 target->next = (*L)->next;
        //2. 新結點做爲頭結點,則釋放原來的頭結點
        
        for (target = *L; target->next != *L; target = target->next);
        temp = *L;
        
        *L = (*L)->next;
        target->next = *L;
        free(temp);
    }else
    {

        //如果刪除其他結點--其他結點
        //1. 找到刪除結點前一個結點target
        //2. 使得target->next 指向下一個結點
        //3. 釋放需要刪除的結點temp
        for(i=1,target = *L;target->next != *L && i != place -1;target = target->next,i++) ;

            temp = target->next;
            target->next = temp->next;
            free(temp);
        }
    
    return OK;
    
}


//4.5 循環鏈表查詢值
int findValue(LinkList L,int value){
    
    int i = 1;
    LinkList p;
    p = L;
    
    //尋找鏈表中的結點 data == value
    while (p->data != value && p->next != L) {
        i++;
        p = p->next;
    }
    
    //當尾結點指向頭結點就會直接跳出循環,所以要額外增加一次判斷尾結點的data == value;
    if (p->next == L && p->data != value) {
        return  -1;
    }
    
    return i;
    
}

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    
    LinkList head;
    int place,num;
    int iStatus;
    
    //iStatus = CreateList(&head);
    iStatus = CreateList2(&head);
    printf("原始的鏈表:\n");
    show(head);
    
//    printf("輸入要插入的位置和數據用空格隔開:");
//    scanf("%d %d",&place,&num);
//    iStatus = ListInsert(&head,place,num);
//    show(head);

    printf("輸入要刪除的位置:");
    scanf("%d",&place);
    LinkListDelete(&head,place);
    show(head);
    
    
    printf("輸入你想查找的值:");
    scanf("%d",&num);
    place=findValue(head,num);
    if(place!=-1)
        printf("找到的值的位置是place = %d\n",place);
    else
        printf("沒找到值\n");
    
    return 0;
}

輸出結果:

Hello, kongyulu!
請輸入新的結點, 當輸入0時結束!
1
2
3
4
5
6
0
原始的鏈表:
    1    2    3    4    5    6
輸入要刪除的位置:1
    2    3    4    5    6
輸入你想查找的值:4
找到的值的位置是place = 3
Program ended with exit code: 0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章