1. 程式人生 > >資料結構學習之雙向連結串列結構

資料結構學習之雙向連結串列結構

注:本文的主要目的是為了記錄自己的學習過程,也方便與大家做交流。轉載請註明來自:

        在前面總結的單向連結串列結構的基礎上,現在開始著手實踐實踐雙向連結串列結構,如果充分理解了單向連結串列資料結構,那對雙向連結串列結構的理解也就不再困難,換個角度而言,雙向連結串列是單向連結串列的擴充套件,如果從資料結構程式碼的定義上來看,雙向連結串列需要維護三個資料內容:資料(data)、前指標(prev)和後指標(next)。與單向連結串列結構相比較,在程式程式碼中需要多維護一個prev指標域,雙向連結串列如下圖所示:

        由此帶來的好處在於:

        1 對連結串列資料的遍歷操作不僅僅能向後遍歷(如單鏈表),而且還能夠向前遍歷尋找元素,對連結串列資料的操作更加靈活。

        2 可以直接刪除某一個數據元素,我認為這是比較重要的一方面,因為對單鏈表而言,如果要刪除某一個數據元素,需要遍歷至此元素之前的一個結點才能刪除,而雙向連結串列可以遍歷到某一元素,然後可以直接對它進行刪除操作。

        和單向連結串列類似,本次內容也主要從以下三個方面來討論雙向連結串列的程式設計:

        一、雙向連結串列結構及介面定義

/*
 * filename: dlist.h
 * author: zhm
 * date: 2012-12-08
 */

#ifndef _DLIST_H
#define _DLIST_H

#include <stdlib.h>

/* define a structure for the list element*/
typedef struct DListElmt_
{
    void *data;
    struct DListElmt_ *prev;
    struct DListElmt_ *next;
}DListElmt;

        上面結構體定義的是雙向連結串列需要維護的每個元素,每個元素中包含三項內容,除此之外,還需要定義一個結構體用於維護整個連結串列,程式碼如下所示:

/* define a structure for the double linked list */
typedef struct DList_
{
    int size;
    void (*destroy)(void *data);
    void (*match)(const void *key1, const void *key2);
    DListElmt *head;
    DListElmt *tail;
}DList;

        對雙向連結串列的資料管理和維護主要有以上內容,其中包含連結串列元素的個數(size),銷燬連結串列的函式指標(destroy), 頭、尾連結串列元素指標。除此之外,還需要定義與連結串列有關的相關操作介面以及巨集定義,程式碼如下所示:

/* define public interface */
void dlist_init(DList *list, void (*destroy)(void *data));
void dlist_destroy(DList *list);
int dlist_ins_prev(DList *list, DListElmt *element, const void *data);
int dlist_ins_next(DList *list, DListElmt *element, const void *data);
int dlist_remove(DList *list, DListElmt *element, void **data);

#define dlist_size(list) ((list)->size) //get the size of the list.
#define dlist_head(list) ((list)->head) //get the head element
#define dlist_tail(list) ((list)->tail) //get the tail element
#define dlist_is_head(element) ((element)->prev == NULL ? 1 : 0) //whether the element is head or not
#define dlist_is_tail(element) ((element)->next == NULL ? 1 : 0) //whether the element is tail or not
#define dlist_data(element) ((element)->data) //get the data of the element
#define dlist_prev(element) ((element)->prev) //get the prev element
#define dlist_next(element) ((element)->next) //get the next element

#endif

        總體來說,與單鏈表的操作差不多,與雙向連結串列相關的操作有:

        (1) 雙向連結串列的初始化;

        (2) 雙向連結串列的銷燬;

        (3) 雙向連結串列的插入元素操作:有前向插入和後向插入之分

        (4) 雙向連結串列元素的刪除;

        (5) 相關巨集定義,主要是為了方便程式碼的維護和操作。

        二、雙向連結串列的介面操作細節實現

        1 雙向連結串列的初始化.

/*
 * filename: dlist.c
 * author:zhm
 * date: 2012-12-08
 */

#include <stdlib.h>
#include <string.h>
#include "dlist.h"

/* dlist_init */
void dlist_init(DList *list, void (*destroy)(void *data))
{
    list->size = 0;
    list->destroy = destroy;
    list->head = NULL;
    list->tail = NULL;

    return;
}

        主要對雙向連結串列結構體的內容賦初始值,這個比較簡單。

        2 雙向連結串列的銷燬

/* dlist_destroy */
void dlist_destroy(DList *list)
{
    void *data;
    while(dlist_size(list) > 0)
    {
        if( dlist_remove(list, dlist_tail(list), (void **)&data) == 0 //from the head to destroy
                && list->destroy != NULL )
        {
            list->destroy(data);
        }
    }

    memset(list, 0, sizeof(DList));
    return;
}

        需要注意的是,此函式內呼叫的是雙向連結串列的元素刪除函式,通過外層的迴圈迭代來刪除每個元素,最終將整個連結串列的大小變為0,退出迭代。在刪除完每個元素後,還需要將元素內的資料域所佔用的空間釋放。關於雙向連結串列的元素刪除函式實現細節(dlist_remove)後面再作說明。

        3 雙向連結串列的插入元素操作(前向插入)

/* dlist_ins_prev */
int dlist_ins_prev(DList *list, DListElmt *element, const void *data)
{
    DListElmt *new_element;

    //Do not allow a NULL unless the list is empty
    if( element == NULL && dlist_size(list) != 0 )
        return -1;

    new_element = (DListElmt *)malloc(sizeof(DListElmt));
    if( new_element == NULL )
        return -1;

    new_element->data = (void *)data;

    if( dlist_size(list) == 0 )
    {
        list->head = new_element;
        list->tail = new_element;
        new_element->prev = NULL;
        new_element->next = NULL;
    }
    else
    {
        new_element->next = element;
        new_element->prev = element->prev;
        
        if( element->prev == NULL )
        {
            list->head = new_element;
        }
        else
        {
            element->prev->next = new_element;
        }
        
        element->prev = new_element;
    }

    /* Adjust the size */
    list->size++;

    return 0;   
}

        所謂前向插入,顧名思義,是從雙向連結串列中某一個元素結點的前面位置插入新的元素,此函式有三個引數,element表示要插入的參考結點位置,需要注意的是:

        若element = NULL, 則雙向連結串列應該為空,否則退出並返-1;

        若element != NULL,則需要在element->prev位置插入元素,插入的新元素的資料域為第三個引數data.另外還需要考慮當element為head結點時的情況

        4 雙向連結串列的插入元素操作(後向插入)

/* dlist_ins_next */
int dlist_ins_next(DList *list, DListElmt *element, const void *data)
{
    DListElmt *new_element;

    //do not allow a NULL unless the list is empty
    if( element == NULL && dlist_size(list) != 0 ) 
        return -1;

    //allocate the memory for the new element.
    new_element = (DListElmt *)malloc(sizeof(DListElmt));
    if( new_element == NULL )
        return -1;

    //fill the data to the element
    new_element->data = (void *)data;

    //insert the element to the list
    if( dlist_size(list) == 0 )
    {
        //the list is empty
        new_element->prev = NULL;
        new_element->next = NULL;
        list->head = new_element;
        list->tail = new_element;
    }
    else
    {
        //the list is not empty
        if( dlist_next(element) == NULL )
        {
            list->tail = new_element;
        }
        else
        {
            new_element->next->prev = new_element;
        }
        new_element->next = element->next;
        new_element->prev = element;
        
        element->next = new_element;
    }

    //adjust the size
    list->size++;
    return 0;
}

        後向插入與前向插入類似,不過與前向不同的是,後向插入時需要注意考慮當插入的參考結點為tail的情況,如上程式碼中if( dlist_next(element) == NULL)時,這主要是一些細節問題,很容易忽略。插入完成後,需要對連結串列元素大小進行累加操作。

        5 雙向連結串列元素的刪除
        額,繼續看程式碼吧!~~

/* dlist_remove */
int dlist_remove(DList *list, DListElmt *element, void **data)
{
    //do not allow a NULL or a empty list
    if( element == NULL || dlist_size(list) == 0 )
        return -1;

    /* remove the element from the list */
    *data = element->data;
    
    if( element == list->head )
    {
        list->head = element->next;
        if( list->head == NULL )
        {
            list->tail = NULL;
        }
        else
        {
            element->next->prev = NULL;
        }
    }
    else
    {
        element->prev->next = element->next;

        if( element->next == NULL ) //be care of the last element;
        {
            list->tail = element->prev;
        }
        else
        {
            element->next->prev = element->prev;
        }
    }

    /* free the sotrage */
    free(element);

    /* adjust the size */
    list->size--;

    return 0;
}

        注意第三個引數,用於儲存被刪除元素中的資料域,之所以儲存這個資料域,是因為在設計時考慮到通過性,此資料域需要由使用者在使用時自己維護,在需要的時候由使用者自己分配和釋放空間,這種設計靈活性比較高而且通用性較好。第二個引數element決定了需要刪除的元素即element本身,前提是雙向連結串列不能為空。並且刪除完後需要釋放元素結點空間,並調整元素大小。此外需要特別注意考慮被刪除的結點是頭結點和尾結點時需要對雙向連結串列的head和tail進行維護。
       

        三、雙向連結串列的應用舉例

        本應用程式比較簡單,主要目的在於應用雙向連結串列的每個介面操作及巨集定義內容。知道如何使用雙向連結串列。在本例中,用一個結構體表示長方體,內含三個變數,分別為長、寬、高,如下所示:

/*
 * filename:main.c
 * author:zhm
 * date:2012-12-08
 */

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#include "dlist.h"

typedef struct Cuboid_
{
    int length;
    int width;
    int height;
}Cuboid;

        此資料型別所產生的變數將作為雙向連結串列元素資料的data資料域。在給每個元素的data資料域賦值之前,將呼叫下面這個例項化函式,用於產生每個Cuboid型別的資料物件,並返回其指標,程式碼如下所示:

Cuboid *cube_instance(const int length, const int width, const int height)
{
    Cuboid *cb_ptr;
    cb_ptr = (Cuboid *)malloc(sizeof(Cuboid));
    if( cb_ptr == NULL )
        return NULL;

    cb_ptr->length = length;
    cb_ptr->width = width;
    cb_ptr->height = height;

    return cb_ptr;
}

        上面函式使用場景如下所示,從main函式中擷取部分程式碼展示。

/* main */
int main(int argc, char **argv)
{
    int i;
    DList dlist_exp;
    DListElmt *p = NULL;
    Cuboid *cb1_ptr, *cb2_ptr, *cb3_ptr, *cb4_ptr, *cb5_ptr;
    Cuboid *cb_ptr;

    //cb1_ptr ~ cb5_ptr are the data of the 5 elements.
    cb1_ptr = cube_instance(1,2,3);
    cb2_ptr = cube_instance(6,10,8);
    cb3_ptr = cube_instance(5,20,30);
    cb4_ptr = cube_instance(17,100,25);
    cb5_ptr = cube_instance(3,6,9);
         ......
}

        如以上程式碼通過呼叫cube_instance()函式產生五個長方體物件,並且每個物件有各自的長寬高數值,每個物件用長方體指標Cuboid *來維護。下面我們對dlist_exp雙向連結串列進行初始化,然後將插入5個元素,每個元素的資料域將一一賦予上述長方體物件指標。程式碼如下所示:

int main(int argc, char **argv)
{
    ......

    //init the double linked list.
    dlist_init(&dlist_exp, destroy);

    //insert the 5 elements into the dlist 
    dlist_ins_next(&dlist_exp, NULL, (void *)cb1_ptr );  //insert data:cb1
    p = dlist_head(&dlist_exp); //get the address of the first element
    dlist_ins_next(&dlist_exp, p , (void *)cb2_ptr );   //insert data:cb2    cb1- cb2
    p = dlist_next(p);          //pointer to the element containing the data cb2.
    dlist_ins_prev(&dlist_exp, p, (void *)cb3_ptr );    //insert data:cb3   cb1- cb3- cb2
    dlist_ins_prev(&dlist_exp, p, (void *)cb4_ptr );    //insert data:cb4   cb1- cb3- cb4- cb2
    p = dlist_prev(p);          //pointer to the element conatining the data cb4.
    dlist_ins_prev(&dlist_exp, p, (void *)cb5_ptr );      //insert data:cb5   cb1- cb3- cb5- cb4- cb2
       ......
}

        請注意插入的順序(前向插入及後向插入,註釋中已經表示插入後連結串列的順序,這裡不再細說。後面我們將展示插入完成後的連結串列遍歷輸出操作,並顯示每個元素的data域值,程式碼如下:

    //now the sequence is: head->cb1->cb3->cb5->cb4->cb2
    printf("traverse and print:\n");
    p = dlist_head(&dlist_exp); //get the head element;
    for( i = 0; i < dlist_size(&dlist_exp); i++ )
    {
        cb_ptr = (Cuboid *)dlist_data(p); //get the element's data, every data is a Cuboid's pointer.
        printf("i = %d: ",i);
        printf("length = %d, width = %d, height = %d\n",
                cb_ptr->length,
                cb_ptr->width,
                cb_ptr->height);
        p = dlist_next(p); //pointer to next element;
    }


        為了展示刪除連結串列中的元素結點操作,這裡打算刪除第3個結點的元素,即包含資料cb5(3,6,9)長方體的元素,刪除後再次遍歷顯示每個元素的data域值,程式碼如下:

    //we'll remove the third element:that's containing the data of cb5(3,6,9)
    p = dlist_head(&dlist_exp);
    p = dlist_next(p);
    p = dlist_next(p);
    dlist_remove(&dlist_exp, p, (void **)&cb_ptr);
    printf("the data of the third element: length = %d, width = %d, height = %d\n",
            cb_ptr->length,
            cb_ptr->width,
            cb_ptr->height);
    destroy(cb_ptr); //free the memory

    //now we'll show you the remained elements,the sequence is :(head)cb1->cb3->cb4->cb2(tail)
    printf("after remove the third elements:\n");
    p = dlist_head(&dlist_exp);
    for(i = 0; i < dlist_size(&dlist_exp); i++ )
    {
        cb_ptr = (Cuboid *)dlist_data(p);
        printf("i = %d: ",i);
        printf("length = %d, width = %d, height = %d\n",
                cb_ptr->length,
                cb_ptr->width,
                cb_ptr->height);
        p = dlist_next(p);
    }

        上述程式碼比較簡單,這裡也不再進行說明。最後就是銷燬連結串列操作,程式碼如下:

   ...... 
   //destroy the double linked list
    dlist_destroy(&dlist_exp);
    printf("after destroy the list,its size = %d\n", dlist_size(&dlist_exp));
   ......

        最後,我將main.c原始碼整個展示出來,有需要的朋友可以自己動手執行一下,加深印象,main.c原始碼如下:

/*
 * filename:main.c
 * author:zhm
 * date:2012-12-08
 */

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#include "dlist.h"

typedef struct Cuboid_
{
    int length;
    int width;
    int height;
}Cuboid;

Cuboid *cube_instance(const int length, const int width, const int height)
{
    Cuboid *cb_ptr;
    cb_ptr = (Cuboid *)malloc(sizeof(Cuboid));
    if( cb_ptr == NULL )
        return NULL;

    cb_ptr->length = length;
    cb_ptr->width = width;
    cb_ptr->height = height;

    return cb_ptr;
}

/*destroy */
void destroy(void *data)
{
    free(data);
    return;
}


/* main */
int main(int argc, char **argv)
{
    int i;
    DList dlist_exp;
    DListElmt *p = NULL;
    Cuboid *cb1_ptr, *cb2_ptr, *cb3_ptr, *cb4_ptr, *cb5_ptr;
    Cuboid *cb_ptr;

    //cb1_ptr ~ cb5_ptr are the data of the 5 elements.
    cb1_ptr = cube_instance(1,2,3);
    cb2_ptr = cube_instance(6,10,8);
    cb3_ptr = cube_instance(5,20,30);
    cb4_ptr = cube_instance(17,100,25);
    cb5_ptr = cube_instance(3,6,9);

    //init the double linked list.
    dlist_init(&dlist_exp, destroy);

    //insert the 5 elements into the dlist 
    dlist_ins_next(&dlist_exp, NULL, (void *)cb1_ptr );  //insert data:cb1
    p = dlist_head(&dlist_exp); //get the address of the first element
    dlist_ins_next(&dlist_exp, p , (void *)cb2_ptr );   //insert data:cb2    cb1- cb2
    p = dlist_next(p);          //pointer to the element containing the data cb2.
    dlist_ins_prev(&dlist_exp, p, (void *)cb3_ptr );    //insert data:cb3   cb1- cb3- cb2
    dlist_ins_prev(&dlist_exp, p, (void *)cb4_ptr );    //insert data:cb4   cb1- cb3- cb4- cb2
    p = dlist_prev(p);          //pointer to the element conatining the data cb4.
    dlist_ins_prev(&dlist_exp, p, (void *)cb5_ptr );      //insert data:cb5   cb1- cb3- cb5- cb4- cb2

    //now the sequence is: head->cb1->cb3->cb5->cb4->cb2
    printf("traverse and print:\n");
    p = dlist_head(&dlist_exp); //get the head element;
    for( i = 0; i < dlist_size(&dlist_exp); i++ )
    {
        cb_ptr = (Cuboid *)dlist_data(p); //get the element's data, every data is a Cuboid's pointer.
        printf("i = %d: ",i);
        printf("length = %d, width = %d, height = %d\n",
                cb_ptr->length,
                cb_ptr->width,
                cb_ptr->height);
        p = dlist_next(p); //pointer to next element;
    }

    //we'll remove the third element:that's containing the data of cb5(3,6,9)
    p = dlist_head(&dlist_exp);
    p = dlist_next(p);
    p = dlist_next(p);
    dlist_remove(&dlist_exp, p, (void **)&cb_ptr);
    printf("the data of the third element: length = %d, width = %d, height = %d\n",
            cb_ptr->length,
            cb_ptr->width,
            cb_ptr->height);
    destroy(cb_ptr); //free the memory

    //now we'll show you the remained elements,the sequence is :(head)cb1->cb3->cb4->cb2(tail)
    printf("after remove the third elements:\n");
    p = dlist_head(&dlist_exp);
    for(i = 0; i < dlist_size(&dlist_exp); i++ )
    {
        cb_ptr = (Cuboid *)dlist_data(p);
        printf("i = %d: ",i);
        printf("length = %d, width = %d, height = %d\n",
                cb_ptr->length,
                cb_ptr->width,
                cb_ptr->height);
        p = dlist_next(p);
    }

    //destroy the double linked list
    dlist_destroy(&dlist_exp);
    printf("after destroy the list,its size = %d\n", dlist_size(&dlist_exp));
    return 0;
}

        通過編譯後,程式的執行結果如下所示: