數據結構1.單鏈表

鏈表與數組

在編程語言中,數組數據結構(array data structure),簡稱數組(Array),是一種數據結構,是數據元素(elements)的集合。有限個相同類型的元素按順序存儲,用一個名字命名,然後用編號區分他們的變量的集合;這個名字稱爲數組名,編號稱爲下標。

鏈表(Linked list)是一種常見的基礎數據結構,是一種線性表,但是不同與數組的是,並不會按線性的順序存儲數據,而是在每一個節點裏存到下一個節點的指針(Pointer)。由於不必須按順序存儲。

一、鏈表與數組的優缺點

首先讓我們來介紹下鏈表與數組之間的優缺點:

// 數組的缺點:
//
//    1.一旦數組定義,則大小固定,無法進行修改(數組的大小)。
//    2.數組插入和刪除的效率太低,時間複雜度O(n)。
//    
// 數組的優點:
//    
//    1.下標訪問,速度快,時間複雜度是O(1)
//
//////////////////////////////////////////////////////////
//
// 鏈表:
//   
// 鏈表的優點:
//   
//    1.資源允許的情況下,規模可以不斷的增長或減小。
//    2.刪除和添加效率高,O(1)
//
// 鏈表的缺點:
//
//    鏈表的遍歷過程效率比較低。

爲什麼會有這樣的差異呢。
接下來我們一步步來看。

二、數據結構類型的定義

數組的定義:

//方法1
int array1[10] = {0};    

//方法2
int array2[] = {12, 23, 34, 45, 56, 67, 78};  

//方法3
int *array3 = NULL;
array3 = (int *)malloc(sizeof(int) * 100);   
if(array3 != NULL){
     fprintf(stderr, "the memory is full!\n");
     exit(1);
}

這裏,我們的array1array2都在定義時直接分配了上的內存空間;
array3是一個int類型的指針,指向我們在上用malloc分配的4bytes ×100 = 400 bytes大小的空間。

我們的數組雖然在O(1)的時間複雜度訪問下標進行數據存取查找,可是一般數組定義後,大小不能夠進行動態變化,且插入刪除效率過低,時間複雜度爲O(n).
所以,爲了彌補這些不足,我們又學習到了鏈表。

鏈表的定義

鏈表是一種物理存儲單元上非連續、非順序的存儲結構,鏈表由相同結構的鏈表節點構成,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。可以關注鏈表節點的結構:

typedef struct List_node
{
    int               data;    //數據域
    struct List_node *next;    //指針域
}List_node;

typedef List_node *List_head;
  1. 鏈表節點的兩個組成部分:
    我們可以看到鏈表節點主要分爲兩大部分:數據域和鏈域。
    數據域是我們最要存儲的內容,而鏈域則代表着一種指針。

  2. 如何把兩個節點進行連接:
    這個指針指向類型爲鏈表節點,這樣每一個節點就可以記錄下下一個節點的位置,從而把原本毫不相干的鏈表節點串接起來。

dlist

一個鏈表節點定義;

    typedef struct Node{
        //數據域
        char name[20];  //姓名
        char sex;       //m 男 w 女 性別
        char age;       //年齡
        char jobs[20];  //職業
        //鏈域
        struct Node *next;
    }Node;

再申請一個鏈表節點;

    Node node1 = {0};

    Node *p_node = (Node *)malloc(sizeof(Node));

    node1.next = p_node;

malloc 函數:
void *malloc(int size);
說明:malloc 向系統申請分配指定size個字節的內存空間。返回類型是 void* 類型。
man malloc可在bash中查看幫助文檔。

三、鏈表的常見接口

我們把可以對鏈表的操作是在.h文件中進行聲明的,我們叫這樣的聲明爲接口,如下是單鏈表的接口聲明:

/* list.h */
/*
 * 帶頭節點的鏈表
 * */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

#define TRUE     (1)
#define FALSE    (0)

typedef unsigned char Boolean;

//定義鏈表節點類型
typedef struct List_node{
    int               data;    //數據域  //4
    struct List_node *next;    //指針域  //32位:4 //64位:8
}List_node;


typedef List_node   *List_head;   

//鏈表的接口
//  常見接口:
//     1.初始化
//     2.銷燬
//     3.增加
//     4.刪除
//     5.查找
//     6.修改
//     7.排序
//     8.顯示
List_head init_list(void)                                        ;   //1. 鏈表的初始化
void      destroy_list(List_head *head)                          ;   //2. 鏈表的銷燬
Boolean   push_front(List_head head, int value)                  ;   //3.1 頭部添加
Boolean   push_back(List_head head, int value)                   ;   //3.2 尾部添加(效率低,從頭找到尾才能添加)
Boolean   pop_front(List_head head)                              ;   //4.1 頭部刪除
Boolean   pop_back(List_head head)                               ;   //4.2 尾部刪除(效率低)
Boolean   find_node(List_head head, int value, List_node **node) ;   //5. 鏈表的查找
void      modify_node(List_node *node, int value)                ;   //6.1 鏈表節點的修改
Boolean   insert_node(List_head head, int index, int value)      ;   //6.2 鏈表節點的插入(下標)
void      sort_list_ascend(List_head head)                       ;    //7.1 鏈表升序
void      sort_list_descend(List_head head)                      ;    //7.2 鏈表降序
void      show_list(List_head head)                              ;    //8.1 顯示鏈表信息
int       get_list_length(List_head head)                        ;    //8.2 鏈表的長度

我們可以通過定義 包裹函數 來縮短程序。每個包裹函數完成實際的函數調用,檢查返回值,並在發生錯誤時終止進程。
包裹函數其實就是封裝函數,調用一個函數來實現這個功能。既然我們會經常用到一些函數並且還要進行錯誤處理,那麼,我們可以將這些函數封裝起來,也放入頭文件.h當中。

//包裹函數
static void *Malloc(size_t size);
static List_node *create_node(void);
static void swap(void *a, void *b, int length);

static void *Malloc(size_t size)
{
    void *result = malloc(size);
    if(result == NULL){   //失敗情況處理
        fprintf(stderr, "the memory is full!\n");
        exit(1);
    }
    return result;
}

static List_node *create_node(void)
{
    //申請節點,並且對節點初始化(置爲0)
    List_node *node = (List_node *)Malloc(sizeof(List_node));
    bzero(node, sizeof(List_node));

    return node;
}
static void swap(void *a, void *b, int length)
{
    void *temp = Malloc(length);

    memcpy(temp, a, length);
    memcpy(a, b, length);
    memcpy(b, temp, length);

    free(temp);
}

四、鏈表接口的實現

爲了能夠使用這些我們所提供的接口,如同菜名需要菜譜一樣,我們要能夠實現這些操作,可以在list.c中進行實現。


//////////////////////////////////////////////////////////////////////////////////////
//接口實現
//////////////////////////////////////////////////////////////////////////////////////

List_head init_list(void)                                           //1. 鏈表的初始化
{
    List_head head = NULL;

    head = create_node();

    return head;
}

void      destroy_list(List_head *head)                             //2. 鏈表的銷燬
{
    List_node *p = NULL;
    List_node *q = NULL;

    if(head == NULL || *head == NULL){
        return ;
    } 

    //從鏈表頭節點開始,依次對節點進行刪除
    p = *head;
    while(p != NULL){
        q = p;
        p = p->next;
        free(q); 
    }

    *head = NULL;
}

Boolean   push_front(List_head head, int value)                      //3.1 頭部添加
{
    List_node *node = NULL;

    if(head == NULL){    //判斷鏈表是否存在
        return FALSE;
    }

    //生成鏈表節點並且賦初值
    node = create_node();
    node->data = value;

    node->next = head->next;
    head->next = node;

    head->data++;  
    return TRUE;
}

Boolean   push_back(List_head head, int value)                      //3.2 尾部添加(效率低,從頭找到尾才能添加)
{
    List_node *p_node = NULL;

    if(head == NULL){    //參數檢測
        return FALSE;
    }  

    //查找尾部節點的位置
    p_node = head;
    while(p_node->next != NULL){
        p_node = p_node->next;
    }

    //把新生成的節點追加到鏈表末尾
    p_node->next = create_node();
    p_node->next->data = value;

    head->data++;
    return TRUE;    
}

Boolean   pop_front(List_head head)                                  //4.1 頭部刪除
{
    List_node *p_node = NULL;

    if(head == NULL || head->next == NULL){
        //鏈表不存在或者沒有有效節點
        return FALSE;
    }

    p_node = head->next;
    head->next = p_node->next;
    free(p_node);

    head->data--;
    return TRUE;
}

Boolean   pop_back(List_head head)                                   //4.2 尾部刪除(效率低)
{
    List_node *p_node = NULL;

    //參數檢測
    if(head == NULL || head->next == NULL){
        return FALSE;
    }

    //從頭結點開始查找倒數第二個節點
    p_node = head;

    while(p_node->next->next != NULL){
        p_node = p_node->next;
    }
    //刪除最後一個節點,把倒數第二個置爲最後一個節點
    free(p_node->next);
    p_node->next = NULL;

    head->data--;
    return TRUE;
}




Boolean   find_node(List_head head, int value, List_node **node)    //5. 鏈表的查找
{
    List_node *p_node = NULL;

    if(head == NULL){
        return FALSE;
    }

    //從第一個有效節點開始查找值爲value的節點
    p_node = head->next;
    while(p_node != NULL){
        if(p_node->data == value){    //找到value所在節點
            if(node != NULL){
                *node = p_node;
            }
            return TRUE;
        }
        p_node = p_node->next;
    }
    return FALSE;
}

void      modify_node(List_node *node, int value)                    //6.1 鏈表節點的修改
{
    node->data = value;
}

Boolean   insert_node(List_head head, int index, int value)   //6.2 鏈表節點的插入
{
    List_node *node = NULL;
    List_node *p_node = NULL;
    int count = index;

    if(head == NULL || index < 0 || index > head->data){
       //鏈表不存在並且下標不符合規則
       return FALSE;
    }

    //創建新的節點
    node = create_node();
    node->data = value;

    //尋找插入的位置
    p_node = head;
    while(count--){   //找到被插入節點的前一個節點
       p_node = p_node->next;
    }

    //插入新的節點
    node->next = p_node->next;
    p_node->next = node;
    head->data++;
    return TRUE;
}

void      sort_list_ascend(List_head head)                           //7.1 鏈表升序
{
    List_node *p_node = NULL;
    List_node *q_node = NULL;

    if(head == NULL || head->data < 2){
        return ;
    }

    for(p_node = head->next; p_node->next ; p_node = p_node->next){ 
        for(q_node = p_node->next; q_node; q_node = q_node->next){
            if(p_node->data > q_node->data){
                swap(p_node, q_node, (unsigned long)(&((List_node *)0)->next));
            }
        }
    }
}

void      sort_list_descend(List_head head)                          //7.2 鏈表降序
{
    List_node *p_node = NULL;
    List_node *q_node = NULL;

    if(head == NULL || head->data < 2){
        return ;
    }

    for(p_node = head->next; p_node->next ; p_node = p_node->next){ 
        for(q_node = p_node->next; q_node; q_node = q_node->next){
            if(p_node->data < q_node->data){
                swap(p_node, q_node, sizeof(List_node) - sizeof(List_node *));
            }
        }
    }
}

void      show_list(List_head head)                                 //8.1 顯示鏈表信息
{
    List_node *p_node = NULL;

    if(head == NULL){
        return ;
    }

    for(p_node = head->next; p_node != NULL; p_node = p_node->next){
        printf("%d  ", p_node->data);
    }
    printf("\n");
}

int       get_list_length(List_head head)    //8.2 鏈表的長度
{
    if(head == NULL){
        return -1;
    }
    return head->data;
}

設計完這個單鏈表的接口與實現之後,我們要對相應功能進行檢測。
測試代碼如下:

//主函數-各個函數的測試
int main(int argc, char ** argv)
{
    int i = 0;
    List_head head = NULL;
    head = init_list();
    List_node *node = create_node();

    printf("\n頭部插入:\n"); 
    for(i = 0 ; i < seed ; i++ )
    {
        push_front(head,rand()%100);
    }
    show_list(head);                   

    printf("\n尾部插入:\n"); 
    for(i = 0 ; i < seed ; i++ )
    {
        push_back(head,rand()%100);
    }
    show_list(head);                   

    printf("\n頭部刪除:\n"); 
    pop_front(head);
    show_list(head);                 

    printf("\n尾部刪除:\n"); 
    pop_back(head);
    show_list(head);                 

    printf("\n插入節點:\n"); 
    insert_node(head, 3, 100);
    show_list(head);                 

    find_node(head, 77,&node);

    printf("\n升序排序:\n");
    sort_list_ascend(head);
    show_list(head);                 

    printf("\n降序排序:\n");
    sort_list_descend(head);
    show_list(head); 

    printf("\n修改節點數據:\n");
    node = head->next->next; 
    modify_node(node, 777);
    show_list(head); 

    printf("\nthe length of this list :%d\n",get_list_length(head));

    destroy_list(&head);
    return 0;
}

運行結果:

root@aemonair# ./my_head_list 
鏈表初始化成功!

頭部插入:
當前鏈表如下:
93 15 77 86 83 

尾部插入:
當前鏈表如下:
93 15 77 86 83 35 86 92 49 21 

頭部刪除:
當前鏈表如下:
15 77 86 83 35 86 92 49 21 

尾部刪除:
當前鏈表如下:
15 77 86 83 35 86 92 49 

插入節點:
當前鏈表如下:
15 77 86 100 83 35 86 92 49 

the node is found !

升序排序:
當前鏈表如下:
15 35 49 77 83 86 86 92 100 

降序排序:
當前鏈表如下:
100 92 86 86 83 77 49 35 15 

修改節點數據:
當前鏈表如下:
100 777 86 86 83 77 49 35 15 

the length of this list :9
鏈表銷燬完畢!

測試程序2:


int main(int argc, char **argv)
{
    List_head head = init_list();   //鏈表的初始化
    int i = 0;
    int value = 0;
    List_node *find = NULL;

    for(i = 0; i < 10; ++i){    //尾部追加10個元素
        push_front(head, rand() % 100);
    }

    pop_front(head);

    pop_back(head);

    show_list(head);   //顯示鏈表信息

    printf("the count of list:%d\n", get_list_length(head));

    printf("你要找哪個節點:?\n");
    scanf("%d", &value);

    find_node(head, value, &find);
    if(find == NULL){
        printf("the %d is not found!\n", value);
    }else{
        printf("found! the value is %d\n", find->data);
    }
    sort_list_ascend(head);
    show_list(head);   //顯示鏈表信息

    sort_list_descend(head);
    show_list(head);   //顯示鏈表信息

    destroy_list(&head);
    return 0;
}

總結:

以上,我們關於單鏈表的基本操作和基本接口的實現。
對於鏈表的指針域在交換兩個的時候很容易出現問題,之後還會進行進一步的詳解。

發佈了38 篇原創文章 · 獲贊 8 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章