链表

10 篇文章 0 订阅

0、引入

链表可以说是一种最为基础的结构。链表是由一组元素以一种特定的顺序组合或链接在一起,在维护数据的集合时很有用。这一点同我们常用到的数组很相似。然而,链表在很多情况下比数组更有优势。特别是在执行插入和删除操作时链表拥有更高的效率。链表需要动态地开辟存储空间,也就是存储空间是在程序运行时分配的。由于在很多应用中数据的大小在编译时并不能确定,因此这种动态分配空间的特性也是链表的一个优点。


1、单链表介绍

单链表(通常简称为链表)由各个元素之间通过一个指针彼此链接起来而组成。每个元素包含两部分:数据成员和一个称为next的指针。通过采用这种二成员结构,将每个元素的next指针设置为指向其后面的元素。最后一个元素的next指针设置为NULL,简单地表示链表的尾端。链表开始处的元素是“头”,链表末尾的元素称为“尾”。

单链表示图

要访问链表中的某个元素,从链表头开始,通过next指针从一个元素到另一个元素连续地遍历直到找到所需要的那个元素为止。以单链表来说,只能以一个方向进行遍历:从头到尾,因为每个元素并没有维护指向其前一个元素的链接。因此,如果从链表头开始移动到某个元素,然后我们又希望访问当前位置之前的某个元素,那么必须再次从头开始(尽管某些时候我们事先知道需要访问这个元素,并保存了指向该元素的指针)。通常,这个缺点不是什么大问题。当有必要的时候,我们可以采用双向链表或者循环链表。

从概念上说,可以把链表想象成一系列连续的元素。然而,由于这些元素是动态分配的(在C语言中调用malloc),因此很重要的一点是,切记这些元素通常实际上都是分散在内存空间中的。元素与元素之间的链接关系只是为了确保所有的元素都可以访问到。带着这种思考,我们将会看到维护元素之间的链接信息时需要特别小心。如果我们错误地丢失了一个链接,则从这个位置开始往后的所有元素都无法访问到了。


2、单链表接口的定义

list_init

——————

void list_init(List *list, void (*destroy)(void *data));

返回值:无

描述:初始化由参数list指定的链表。该函数必须在链表做其他操作之前调用。当调用list_destroy时,destroy参数提供了一种释放动态分配的数据的方法。例如,如果链表包含采用malloc动态分配的数据,当链表被销毁时,destroy应该设置为free用来释放数据。对于包含好几个动态分配成员的结构化数据,destroy应该设置为一个用户自定义的析构函数,通过对每一个动态分配的成员以及对结构体自身调用free来释放数据。如果链表包含不应该释放的数据,destroy应该设置为NULL。

复杂度:O(1)


list_destroy

——————

void list_destroy(List *list);

返回值:无

描述:销毁由参数list指定的链表。调用list_destroy后任何其他的操作都不允许执行,除非再次调用list_init。list_destroy将链表中所有的元素都移除,如果传给list_init的参数destroy不为NULL,则移除链表中的每个元素时都调用该函数一次。

复杂度:O(n),n代表链表中的元素个数。


list_ins_next

——————

int list_ins_next(List *list, ListElmt *element, const void *data);

返回值:如果插入元素成功则返回0,否则返回-1。

描述:在list指定的链表中element后面插入一个新元素。如果element设置为NULL,则新元素插入链表头部。新元素包含一个指向data的指针,因此只要该元素还在链表中,data所引用的内存空间就应该保持合法。管理data所引用的存储空间是调用者的责任。

复杂度:O(1)


list_rem_next

——————

int list_rem_next(List *list, ListElmt *element, void **data);

返回值:如果移除元素成功则返回0,否则返回-1。

描述:移除由list指定的链表中element后的那个元素。如果element设置为NULL,则移除链表头元素。调用返回后,data指向已移除元素中存储的数据。由调用者负责管理data所引用的存储空间。

复杂度:O(1)


list_size

——————

int list_size(const List *list);

返回值:链表中元素的个数。

描述:这是一个宏,用来计算由参数list指定的链表中元素的个数。

复杂度:O(1)


list_head

——————

ListElmt *list_head(const List *list);

返回值:指向链表中头元素的指针。

描述:这是一个宏,返回由参数list指定的链表中头元素的指针。

复杂度:O(1)


list_tail

——————

ListElmt *list_tail(const List *list);

返回值:指向链表中尾元素的指针。

描述:这是一个宏,返回由参数list指定的链表中尾元素的指针。

复杂度:O(1)


list_is_head

——————

int list_is_head(const ListElmt *element);

返回值:如果element所指定的元素是链表头结点则返回1,,否则返回-1。

描述:这是一个宏,用来判断由element所指定的元素是否是链表的链表头结点。

复杂度:O(1)


list_is_tail

——————

int list_is_tail(const ListElmt *element);

返回值:如果element所指定的元素是链表末尾结点则返回1;否则返回-1。

描述:这是一个宏,用来判断element所指定的链表元素是否为链表的末尾结点。

复杂度:O(1)


list_data

——————

void *list_data(const ListElmt *element);

返回值:结点中保存的数据。

描述:这是一个宏,返回由element所指定的链表结点元素中保存的数据。

复杂度:O(1)


list_next

——————

ListElmt *list_next(const ListElmt *element);

返回值:返回由element所指定的结点的下一个结点。

描述:这是一个宏,返回链表中由element所指定的结点的下一个结点。

复杂度:O(1)


3、单链表的实现与分析

回顾一下链表元素的组成:一个数据成员和一个指向链表中下一个元素的指针。结构体ListElmt表示链表中的单个元素。如你所料,这个结构体拥有两个成员,就是前面介绍的数据成员和指针成员。结构体List则表示链表这种数据结构。这个结构体由5个数据成员组成:size表示链表中的元素个数;match并不由链表本身使用,而是由从链表数据结构派生而来的新类型所使用;destroy是封装之后传递给list_init的析构函数;head是指向链表中头结点元素的指针;tail则是指向链表中末尾结点元素的指针。

// 链表抽象数据类型的头文件

/* list.h */

#ifndef LIST_H
#define LIST_H

#include <stdlib.h>

/* Define a structure for linked list elements */
typedef struct ListElmt_
{
    void *data;
    struct ListElmt_ *next;
} ListElmt;

/* Define a structure for linked lists */
typedef struct List_
{
    int size;
    int (*match)(const void *key1, const void *key2);
    void (*destroy)(void *data);
    ListElmt *head;
    ListElmt *tail;
} List;

/* Public Interface */
void list_init(List *list, void (*destroy)(void *data));
void list_destroy(List *list);
int list_ins_next(List *list, ListElmt *element, const void *data);
int list_rem_next(List *list, ListElmt *element, void **data);
#define list_size(list) ((list)->size)

#define list_head(list) ((list)->head)
#define list_tail(list) ((list)->tail)
#define list_is_head(list, element) ((element) == (list)->head ? 1 : 0)
#define list_is_tail(element) ((element)->next == NULL ? 1 : 0)
#define list_data(element) ((element)->data)
#define list_next(element) ((element)->next)

#endif // LIST_H

// 链表抽象数据类型的实现

/* list.c */

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

#include "list.h"

/* list_init */
void list_init(List *list, void (*destroy)(void *data)) {
    /* Initialize the list */

    list->size = 0;
    list->destroy = destroy;
    list->head = NULL;
    list->tail = NULL;

    return;
}

/* list_destroy */
void list_destroy(List *list) {
    void *data;

    /* Remove each element */
    while(list_size(list) > 0) {
        if(list_rem_next(list, NULL, (void **)&data) == 0 &&
                list->destroy != NULL) {
            /* Call a user-defined function to free dynamically allocated data. */

            list->destroy(data);
        }
    }

    /* No operations are allowed now, but clear the structure as a precaution. */
    memset(list, 0, sizeof(List));
    return;
}

/* list_ins_next */
int list_ins_next(List *list, ListElmt *element, const void *data) {
    ListElmt *new_element;

    /* Allocate storage for the element */
    if((new_element = (ListElmt *)malloc(sizeof(ListElmt))) == NULL)
        return -1;

    /* Insert the element into the list */
    new_element->data = (void *)data;
    if(element == NULL) {

        /* Handle insertion at the head of the list */
        if(list_size(list) == 0)
            list->tail = new_element;

        new_element->next = list_head;
        list_head = new_element;
    }
    else {
        /* Handle insertion somewhere other than at the head */
        if(element->next == NULL)
            list->tail = new_element;

        new_element->next = element->next;
        element->next = new_element;
    }

    /* Adjust the size of the list to account for the inserted element */
    list->size++;

    return 0;
}

/* list_rem_next */
int list_rem_next(List *list, ListElmt *element, void **data) {
    ListElmt *old_element;

    /* Do not allow removal from an empty list */
    if(list_size(list) == 0)
        return -1;

    /* Remove the element from the list */
    if(element == NULL) {
        /* Handle removal from the head of the list */
        *data = list->head->data;
        old_element = list->head;
        list->head = list->head->next;

        if(list_size(list) == 1)
            list->tail = NULL;
    }
    else {
        /* Handle removal from somewhere other than the head. */
        if(element->next == NULL)
            return -1;

        *data = element->next->data;
        old_element = element->next;
        element->next = element->next->next;

        if(element->next == NULL)
            list->tail = element;
    }

    /* Free the storage allocated by the abstract datatype */
    free(old_element);

    /* Adjust the size of the list to account for the removed element */
    list->size--;
    return 0;
}


list_init

list_init用来初始化一个链表以便能够执行其他的操作。初始化链表是一种简单的操作,只要把链表的size成员设置为0,把函数指针成员destroy设置为定义的析构函数,head和tail指针全设置为NULL即可。

list_init的复杂度为O(1),因为初始化过程中的所有步骤都能在一段恒定的时间内完成。


list_destroy

list_destroy用来销毁链表,其意义就是移除链表中的所有元素。如果调用list_init时destroy参数不为NULL,则当每个元素被移除时都将调用destroy一次。

list_destroy的运行时复杂度为O(n),n代表链表中的元素个数,这是因为list_rem_next的复杂度为O(1),而移除每个元素时都将调用list_rem_next一次。


list_ins_next

list_ins_next将一个元素插入由element参数所指定的元素之后。该调用将新元素的数据指向由用户传递进来的数据。向链表中插入新元素的处理步骤很简单,但需要特别小心。有两种情况需要考虑:插入链表头部和插入其他位置。

一般来说,要把一个元素插入链表中,需要将新元素的next指针指向它之后的那个元素,然后将新元素之前的结点next指针指向新插入的元素。但是,当从链表头部插入时,新元素之前没有别的结点了。因此,在这种情况下,将新元素的next指针指向当前链表的头部,然后重置头结点指针,使其指向新元素。回顾之前的接口设计,当传入的element为NULL时代表新的元素将插入链表头部。另外需要注意的是,无论何时当插入的元素位于链表末尾时,都必须更新链表数据结构的tail成员使其指向新的结点。最后,更新统计链表中结点个数的size成员。

insert


list_rem_next

list_rem_next从链表中移除由element所指定的元素之后的那个结点。移除element之后的那个结点而不是移除element本身。同插入结点类似,这个调用也需要考虑两个因素:移除的是头结点以及移除其余位置上的结点。

移除操作是很简单的,但同样需要注意一些细节问题。一般来说,为了从链表中移除一个元素,将要移除的目标结点前一个元素的next指针指向目标结点的下一个元素。但是,当移除的目标结点是头结点时,目标结点之前并没有其他元素了。因此,在这种情况下,只需要将链表的head成员指向目标结点的下一个元素。同插入操作一样,当传入的element为NULL时,就代表链表的头结点需要移除。另外,无论何时当移除的目标结点是链表的尾部结点时,都必须更新链表数据结构中的tail成员,使其指向新的尾结点,或者当移除操作使得整个链表成为空链表时需要把tail设置为NULL。最后,更新链表的size成员,使其减1。当这个调用返回时,data将指向已移除结点的数据域。

remove

list_rem_next的复杂度为O(1),因为所有的移除步骤都在恒定的时间内完成。


list_size、list_head、list_tail、list_is_tail、list_is_head、list_data以及list_next

这些宏实现了链表中的一些简单操作。一般来说,它们提供了快速访问和检测List和ListElmt结构体中成员的能力。

这些操作的运行时复杂度都是O(1),因为访问和检测结构体的成员都可以在恒定时间内完成。


4、使用链表的例子:页帧管理

在一些支持虚拟内存的系统中有关于链表的一种应用。虚拟内存是一种地址空间的映射机制,它允许进程(运行的程序)不必完全加载到物理内存(系统的实际内存)中也可以得到运行。这种方式的一个优点是进程可以使用比系统实际所允许的物理内存大得多的地址空间。另一个优点是多个进程能够共享系统的内存以并发的方式执行。

运行在虚拟内存机制下的进程需要处理虚拟地址。这些地址对于进程来说就像是物理地址一样,但使用前必须由操作系统做转换。采用由专门的硬件所支持的页表来快速执行地址转换的工作。每一个进程都有它自己的页表,将它的虚拟地址空间中的页映射到物理内存中的页帧上。当某个进程引用一个虚拟地址时,页表中的某项需要检查并决定该页关联到哪个物理页帧上(见下图)。当进程引用一个不在物理页帧上的虚拟地址时,会导致系统产生一个页错误并为之在物理内存中分配一个页帧。为什么进程的页面会从物理内存中移除是另一个问题。一种导致进程页被移除的场景是:当访问某个页面的频率同其他页面相比很低时,而且在别处用到该页帧的情况下。

虚拟内存映射到物理内存

下面这个例子就是针对刚刚描述的页帧管理所设计的。为此,将介绍两个函数,alloc_frame和free_frame。alloc_frame和free_frame采用链表来维护可供分配的页帧。函数alloc_frame从空闲页帧链表中获取空闲页帧号。给定某个特定的页,将页帧号放到页表中来检查该页面应该对应哪个物理页帧。一旦某个页面从物理内存中移除后,函数free_frame接受一个页帧号并将其放回到空闲页帧链表中。这两个函数假定在执行之前,操作系统已经将所有的空闲页帧都插入空闲页帧链表中了。

用链表来管理页帧是一种非常好的方法,因为页帧的分配将涉及频繁的插入和删除操作,而且这些操作都发生在链表头。alloc_frame和free_frame的运行时复杂度都是O(1),因为这两个函数都分别只是简单地调用list_rem_next以及list_ins_next,而这两个函数在前面已经分析过了,它们都是复杂度为O(1)的操作。

// 页帧管理的函数实现

/* frames.c */

#include <stdlib.h>

#include "frames.h"
#include "list.h"

/* alloc_frame */
int alloc_frame(List *frames)
{
    int frame_number, *data;

    if(list_size(frames) == 0)
    {
        /* Return that there are no frames available. */
        return -1;
    }
    else
    {
        if(list_rem_next(frames, NULL, (void **)&data) != 0)
        {
            /* Return that a frame could not be retrieved. */
            return -1;
        }
        else
        {
            /* Store the number of the available frame. */
            frame_number = *data;
            free(data);
        }
    }

    return frame_number;
}

/* free_frame */
int free_frame(List *frames, int frame_number)
{
    int *data;

    /* Allocate storage for the frame number. */
    if((data = (int *)malloc(sizeof(int))) == NULL)
        return -1;

    /* Put the frame back in the list of available frames. */
    *data = frame_number;

    if(list_ins_next(frames, NULL, data) != 0)
        return -1;

    return 0;
}


5、双向链表介绍

双向链表,如同其名字所暗示的那样,链表元素之间由两个指针链接。双向链表中的每个元素都由3部分组成:除了数据成员和next指针外,每个元素还包含一个指向其前驱元素的指针,称为prev指针。双向链表的组成是这样的:将一些元素链接在一起使得每个元素的next指针都指向其后继的元素,而每个元素的prev指针都指向其前驱元素。为了标识链表的头和尾,将第一个元素的prev指针和最后一个元素的next指针设置为NULL。

要反向遍历整个双向链表,使用prev指针以从尾到头的顺序连续访问各个元素。因此,为每个元素增加一个指针的代价,换来的是双向链表比单链表提供了更为灵活的访问方式。当我们知道某个元素存储在链表中的某处时,我们可以明智地选择按照何种方式访问到它,这会非常有帮助。例如,双向链表的一种灵活性在于它提供了一种比单链表更直观的方式以移除一个元素。


6、双向链表接口的定义

dlist_init

——————

void dlist_init(DList *list, void (*destroy)(void *data));

返回值:无

描述:初始化由参数list所指定的双向链表。该函数必须在双向链表做其他任何操作之前调用。当调用dlist_destroy时,这里传入的destroy参数提供了一种释放动态分配空间的方法。它的工作方式同前面叙述的list_destroy。对于双向链表,如果其中包含不需要手动释放的空间的数据,destroy参数应该设置为NULL。

复杂度:O(1)


dlist_destroy

——————

void dlist_destroy(DList *list);

返回值:无

描述:销毁由参数list所指定的双向链表。调用该函数后不允许再执行其他操作,除非用户再次调用dlist_init。dlist_destroy函数将移除双向链表中的所有元素,如果传给dlist_init的参数destroy不为NULL,则调用destroy所指定的函数,对链表中每个移除的元素数据施行资源回收操作。

复杂度:O(n),这里n代表了双向链表中的元素个数。


dlist_ins_next

——————

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

返回值:如果插入操作成功则返回0;否则返回-1。

描述:将元素插入由list指定的双向链表中element元素之后。当插入空链表中时,element可能指向任何位置,为了避免混淆,element此时应该设置为NULL。新的元素包含一个指向data的指针,因此只要该元素仍在链表中,data所引用的内存空间就应该保持合法。由调用者负责管理data所引用的存储空间。

复杂度:O(1)


dlist_ins_prev

——————

int dlist_ins_prev(DList *list, SListElmt *element, const void *data);

返回值:如果插入操作成功则返回0,;否则返回-1。

描述:将元素插入由list指定的双向链表中element元素之前。当插入空链表中时,element可能指向任何位置,为了避免混淆,element此时应该设置为NULL。新的元素包含一个指向data的指针,因此只要该元素仍在链表中时,data所引用的内存空间就应该保持合法。由调用者负责管理data所引用的存储空间。

复杂度:O(1)


dlist_remove

——————

int dlist_remove(DList *list, DListElmt *element, void **data);

返回值:如果移除操作成功则返回0;否则返回-1。

描述:从由list指定的双向链表中移除由element所指定的元素。函数返回后,参数data将指向已移除元素中存储的数据域。由调用者负责管理data所引用的存储空间。

复杂度:O(1)


dlist_size

——————

int dlist_size(const DList *list);

返回值:链表中的元素个数。

描述:这是一个宏,用来计算由参数list所指定的双向链表中的元素个数。

复杂度:O(1)


dlist_head

——————

DListElmt *dlist_head(const DList *list);

返回值:返回链表的头元素。

描述:这是一个宏,用来返回由参数list所指定的双向链表中的头元素。

复杂度:O(1)


dlist_tail

——————

DListElmt *dlist_tail(const DList *list);

返回值:返回链表的尾元素。

描述:这是一个宏,用来返回由参数list所指定的双向链表中的尾元素。

复杂度:O(1)


dlist_is_head

——————

int dlist_is_head(const DListElmt *element);

返回值:如果由element所指定的元素是链表头元素则返回1;否则返回0。

描述:这是一个宏,用来判断由参数element所指定的元素是否是链表头元素。

复杂度:O(1)


dlist_is_tail

——————

int dlist_is_tail(const DListElmt *element);

返回值:如果由参数element所指定的元素是链表尾元素则返回1;否则返回0。

描述:这是一个宏,用来判断由参数element所指定的元素是否是链表尾元素。

复杂度:O(1)


dlist_data

——————

void *dlist_data(const DListElmt *element);

返回值:返回由element所指定的链表元素的数据域。

描述:这是一个宏,用来返回由参数element所指定的双向链表元素的数据域。

复杂度:O(1)


dlist_next

——————

DListElmt *dlist_next(const DListElmt *element);

返回值:返回由element所指定的元素的下一个元素。

描述:这是一个宏,用来返回由参数element所指定的链表元素的后继元素。

复杂度:O(1)


dlist_prev

——————

DListElmt *dlist_prev(const DListElmt *element);

返回值:返回由element所指定的元素的前驱元素。

描述:这是一个宏,用来返回双向链表中有element所指定的元素的前驱元素。

复杂度:O(1)


7、双向链表的实现与分析

双向链表由一个数据成员、一个指向下一个元素的next指针,以及一个指向前一个元素的prev指针组成。数据结构DListElmt代表双向链表中的元素。如你所料,这个结构体拥有前文描述过的3个成员。数据结构DList代表双向链表数据结构,该结构的成员同前面介绍的单链表相似。

// 双向链表抽象数据类型的头文件

/* dlist.h */

#ifndef DLIST_H
#define DLIST_H

#include <stdlib.h>

/* Define a structure for doubly-linked list elements. */
typedef struct DListElmt_
{
    void *data;
    struct DListElmt_ *prev;
    struct DListElmt_ *next;
} DListElmt;

/* Define a structure for doubly-linked lists. */
typedef struct DList_
{
    int size;
    int (*match)(const void *key1, const void *key2);
    void (*destroy)(void *data);
    DListElmt *head;
    DListElmt *tail;
} DList;

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

#define dlist_size(list) ((list)->size)
#define dlist_head(list) ((list)->head)
#define dlist_tail(list) ((list)->tail)
#define dlist_is_head(element) ((element)->prev == NULL ? 1 : 0)
#define dlist_is_tail(element) ((element)->next == NULL ? 1 : 0)
#define dlist_data(element) ((element)->data)
#define dlist_next(element) ((element)->next)
#define dlist_prev(element) ((element)->prev)

#endif // DLIST_H

// 双向链表抽象数据类型的实现

/* dlist.c */

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

#include "dlist.h"

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

    return;
}

/* dlist_destroy */
void dlist_destroy(DList *list)
{
    void *data;

    /* Remove each element. */
    while(dlist_size(list) > 0)
    {
        if(dlist_remove(list, dlist_tail(list), (void **)&data) == 0
           && list->destroy != NULL)
        {
            /* Call a user-defined function to free dynamically allocated data. */
            list->destroy(data);
        }
    }

    /* No operations are allowed now, but clear the structure as a precaution. */
    memset(list, 0, sizeof(DList));
    return;
}

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

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

    /* Allocate storage for the element. */
    if((new_element = (DListElmt *)malloc(sizeof(DListElmt))) == NULL)
        return -1;

    /* Insert the new element into the list. */
    new_element->data = (void *)data;
    if(dlist_size(list) == 0)
    {
        /* Handle insertion when the list is empty. */
        list->head = new_element;
        list->head->prev = NULL;
        list->head->next = NULL;
        list->tail = new_element;
    }
    else
    {
        /* Handle insertion when the list is not empty.*/
        new_element->next = element->next;
        new_element->prev = element;

        if(element->next == NULL)
            list->tail = new_element;
        else
            element->next->prev = new_element;

        element->next = new_element;
    }

    /* Adjust the size of the list to account for the inserted element. */
    list->size++;
    return 0;
}

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

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

    /* Allocate storage to be managed by the abstract datatype. */
    if((new_element = (DListElmt *)malloc(sizeof(DListElmt))) == NULL)
        return -1;

    /* Insert the new element into the list. */
    new_element->data = (void *)data;
    if(dlist_size(list) == 0)
    {
        /* Handle insertion when the list is empty. */
        list->head = new_element;
        list->head->prev = NULL;
        list->head->next = NULL;
        list->tail = new_element;
    }
    else
    {
        /* Handle insertion when the list is not empty. */
        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 of the list to account for the new element. */
    list->size++;
    return 0;
}

/* dlist_remove */
int dlist_remove(DList *list, DListElmt *element, void **data)
{
    /* Do not allow a NULL element or removal from an empty list. */
    if(element == NULL || dlist_size(list) == 0)
        return -1;
    
    /* Remove the element from the list. */
    *data = element->data;
    
    if(element == list->head)
    {
        /* Handle removal from the head of the list. */
        list->head = element->next;
        
        if(list->head == NULL)
            list->tail == NULL;
        else
            element->next->prev = NULL;
    }
    else
    {
        /* Handle removal from other than the head of the list. */
        element->prev->next = element->next;
        if(element->next == NULL)
            list->tail = element->prev;
        else
            element->next->prev = element->prev;
    }
    
    /* Free the storage allocated by the abstract datatype. */
    free(element);
    
    /* Adjust the size of the list to account for the removed element. */
    list->size--;
    return 0;
}


dlist_init

dlist_init操作用来初始化一个双向链表以便稍后能够执行其他操作。初始化过程同单链表一样。dlist_init的运行时复杂度为O(1),因为初始化过程中的所有步骤都能在恒定的时间内完成。


dlist_destroy

dlist_destroy操作用来销毁一个双向链表。总的来说,该操作意味着将链表中的所有元素移除。当传递给该函数的destroy参数不为NULL时每当一个元素被移除时都将调用destroy。

dlist_destroy的运行时复杂度为O(n),这里n代表链表中的元素个数。这是因为对于每个元素的移除操作都需要调用一次复杂度为O(1)的dlist_remove函数。


dlist_ins_next

dlist_ins_next操作用来将新元素插入由参数element所指定的元素之后。双向链表中的插入操作同单链表中的很相似。最大的区别在于除了要管理next指针的指向外,还必须管理prev指针,以保证链接的反向链接关系正确(见下图)。

dlist_insert_next

dlist_ins_next的复杂度为O(1),因为插入操作的所有步骤均能够在恒定的时间内完成。


dlist_ins_prev

dlist_ins_prev操作用来将新元素插入双向链表中由参数element所指定的元素之前。双向链表中的插入操作与单链表类似。和dlist_ins_next一样,最大的不同在于除了要管理next指针的指向外,还必须管理prev指针,以保证链表的反向链接关系正确。dlist_ins_prev的运行时复杂度为O(1),因为插入操作的所有步骤均能够在恒定的时间内完成。


dlist_remove

dlist_remove操作将指定的元素从双向链表中移除。同单链表中移除操作的最大不同在于除了要管理next指针外,还必须管理prev指针,以保证链表的反向链接关系正确。对于双向链表来说,还有一个不同的地方在于:该接口可以移除当前指定的元素,而不是移除指定元素之后的那个元素,因为每个元素都有一个指向其前驱元素的指针。

dlist_remove的复杂度为O(1),因为所有的操作步骤都能在恒定时间内完成。


dlist_size、dlist_head、dlist_tail、dlist_is_head、dlist_is_tail、dlist_next以及dlist_prev

这些宏实现双向链表一些简单的操作。总的来说,这些宏提供了访问和测试数据结构DList以及DListElmt的成员的接口。

这里所有的宏的运行时复杂度都为O(1),因为访问和检测数据结构中的成员是一个简单的操作,它们都能够在恒定的时间内完成。


8、循环链表介绍

循环链表是另一种形式的链表,它提供了更为灵活的遍历链表元素的能力。循环链表可以是单向的或双向的,但区分一个链表是不是循环链表只要看它有没有尾部元素即可。在循环链表中,最后一个元素的next指针又指回头元素而不是设置为NULL。在双向循环链表中,头元素的prev指针则指向最后一个元素,这使得循环链表中的每个元素既可以看做头元素也可以看做尾元素。

和单链表和双向链表不同,在遍历循环链表元素的时候不需要担心到达链表尾部而无法继续遍历下去。相反,遍历过程会继续从链表头元素开始,如果是双向循环链表,还可以继续从尾部元素反向遍历。这种形式遍历就形成了一种循环模式(见下图),这也是循环链表得名的原因。

circle_list

在这里介绍的循环链表是单向循环链表。因此,我们需要考虑的仅仅是维护尾元素与头元素的关系,使尾元素的next指针指向头元素。在实际应用中到底是选择单向循环链表还是双向循环链表,视具体情况而定。


9、循环链表接口的定义

clist_init

——————

void clist_init(CList *list, void (*destroy)(void *data));

返回值:无

描述:用来初始化由参数list所指定的循环链表。该函数必须在循环链表做任何其他操作之前调用。当clist_destroy被调用时,这里传入的destroy参数提供了一种释放动态分配空间的方法。它的工作方式同前面叙述的list_destroy相似。对于循环链表,如果其中包含不需要手动释放空间的数据,则destroy参数应该设置为NULL。

复杂度:O(1)


clist_destroy

——————

void clist_destroy(CList *list);

返回值:无

描述:销毁由参数list所指定的循环链表。调用clist_destroy之后,其他任何操作都允许再执行,除非用户再次调用clist_init。函数clist_destroy将循环链表中的所有元素都移除。如果传给clist_init的参数destroy不为NULL时,则调用destroy所指定的函数,对链表中每个移除的元素数据施行资源回收操作。

复杂度:O(n),这里n代表循环链表中的元素个数。


clist_ins_next

——————

int clist_ins_next(CList *list, CListElmt *element, const void *data);

返回值:如果插入操作成功则返回0;否则返回-1。

描述:将元素插入由list指定的循环链表中element之后。当插入空链表中时,element可能指向任何位置,为了避免混淆,element此时应该设置为NULL。新的元素包含一个指向data的指针,因此只要该元素仍在链表中时,data所引用的内存空间就应该保持合法。由调用者负责管理data所引用的存储空间。

复杂度:O(1)


clist_rem_next

——————

int clist_rem_next(CList *list, CListElmt *element, void **data);

返回值:如果移除操作成功则返回0;否则返回-1。

描述:移除由参数list指定的循环链表中element后面的元素。返回时,参数data将指向已移除元素中存储的数据。由调用者负责管理与data相关联的存储空间。

复杂度:O(1)


clist_size

——————

int clist_size(const CList *list);

返回值:链表中的元素个数。

描述:这是一个宏,用来计算由参数list所指定的链表中的元素个数。

复杂度:O(1)


clist_head

——————

CListElmt *clist_head(const CList *list);

返回值:返回链表头的元素。

描述:这是一个宏,用来返回由参数list所指定的循环链表中的头元素。

复杂度:O(1)


clist_data

——————

void *clist_data(const CListElmt *element);

返回值:返回元素中存储的数据域。

描述:这是一个宏,用来返回循环链表中由参数element所指定的元素中存储的数据域。

复杂度:O(1)


clist_next

——————

CListElmt *clist_next(const CListElmt *element);

返回值:返回由element所指定的元素的后继元素。

描述:这是一个宏,用来返回循环链表中由参数element所指定的元素的后继元素。

复杂度:O(1)


10、循环链表的实现与分析

同单链表一样,循环链表中的每个元素也包含两部分:一个数据域指针和一个指向后继元素的next指针。数据结构CListElmt代表循环链表中的单独元素。如你所料,这个结构体中拥有两个成员,与前面叙述的一致。数据结构CList代表循环链表。这个结构体同单链表类似,但它不包含tail成员。

// 循环链表抽象数据类型的头文件

/* clist.h */

#ifndef CLIST_H
#define CLIST_H

#include <stdlib.h>

/* Define a structure for circular list elements. */
typedef struct CListElmt_
{
    void *data;
    struct CListElmt_ *next;
} CListElmt;

/* Define a structure for circular lists. */
typedef struct CList_
{
    int size;
    int (*match)(const void *key1, const void *key2);
    void (*destroy)(void *data);
    CListElmt *head;
} CList;

/* Public Interface */
void clist_init(CList *list, void (*destroy)(void *data));
void clist_destroy(CList *list);
int clist_ins_next(CList *list, CListElmt *element, const void *data);
int clist_rem_next(CList *list, CListElmt *element, void **data);

#define clist_size(list) ((list)->size)
#define clist_head(list) ((list)->head)
#define clist_data(element) ((element)->data)
#define clist_next(element) ((element)->next)

#endif // CLIST_H

// 循环链表抽象数据类型的实现

/* clist.c */
#include <stdlib.h>
#include <string.h>

#include "clist.h"

/* clist_init */
void clist_init(CList *list, void (*destroy)(void *data))
{
    /* Initialize the list. */
    list->size = 0;
    list->destroy = destroy;
    list->head = NULL;

    return;
}

/* clist_destroy */
void clist_destroy(CList *list)
{
    void *data;

    /* Remove each element. */
    while(clist_size(list) > 0)
    {
        if(clist_rem_next(list, list_head, (void **)&data) == 0
           && list->destroy != NULL)
        {
            /* Call a user-defined function to free dynamically allocated data. */
            list->destroy(data);
        }
    }

    /* No operations are allowed now, but clear the structure as a precaution. */
    memset(list, 0, sizeof(CList));
    return;
}

/* clist_ins_next */
int clist_ins_next(CList *list, CListElmt *element, const void *data)
{
    CListElmt *new_element;

    /* Allocate storage for the element. */
    if((new_element = (CListElmt *)malloc(sizeof(CListElmt))) == NULL)
        return -1;

    /* Insert the element into the list. */
    new_element->data = (void *)data;
    if(clist_size(list) == 0)
    {
        /* Handle insertion when the list is empty. */
        new_element->next = new_element;
        list_head = new_element;
    }
    else
    {
        /* Handle insertion when the list is not empty. */
        new_element->next = element->next;
        element->next = new_element;
    }

    /* Adjust the size of the list to account for the inserted element. */
    list->size++;
    return 0;
}

/* clist_rem_next */
int clist_rem_next(CList *list, CListElmt *element, void **data)
{
    CListElmt *old_element;

    /* Do not allow removal from an empty list. */
    if(clist_size(list) == 0)
        return -1;

    /* Remove the element from the list. */
    *data = element->next->data;
    if(element->next == element)
    {
        /* Handle removing the last element. */
        old_element = element->next;
        list->head = NULL;
    }
    else
    {
        /* Handle removing other than the last element. */
        old_element = element->next;
        element->next = element->next->next;
        if(old_element == clist_head(list))
            list->head = old_element->next;
    }

    /* Free the storage allocated by the abstract datatype. */
    free(old_element);

    /* Adjust the size of the list to account for the removed element. */
    list->size--;
    return 0;
}


clist_init

clist_init操作用来初始化一个循环链表以便稍后能够执行其他操作。初始化过程同非循环的单链表一样,只是这里循环链表没有tail成员而已。

clist_init的运行时复杂度为O(1),因为初始化过程中的所有步骤都能在恒定的时间内完成。


clist_destroy

clist_destroy用来销毁一个循环链表。总的来说,该操作意味着将移除链表中的所有元素。当传递给该函数的destroy不为NULL时,每移除一个元素时都将调用clist_init一次。

clist_destroy的运行时复杂度为O(n),这里n代表链表中的元素个数。这是因为对于每个元素的移除操作都需要调用一次复杂度为O(1)的clist_rem_next函数。


clist_ins_next

clist_ins_next用来将新元素插入循环链表中由参数element所指定的元素之后。将元素插入单向循环链表中的过程同非循环的单链表类似。最大的不同在于当所插入的链表是空链表时,在这种情况下,必须将所插入元素的next指针设置为指向它自己。这就允许循环遍历只有一个元素的链表。这也确保了之后元素的插入操作能够按照恰当的方式进行。

clist_ins_next的运行时复杂度为O(1),因为插入操作中的所有步骤都能够在恒定的时间内完成。


clist_rem_next

clist_rem_next将参数element所指定的元素的后继元素从链表中移除。从单向循环链表中移除元素的过程同非循环的单链表类似。

clist_rem_next的运行时复杂度为O(1),因为移除操作中的所有步骤都能够在恒定的时间内完成。


clist_size、clist_head、clist_data以及clist_next

这些宏实现循环链表中的一些简单操作。总的来说,这些宏提供了访问和检测数据结构CList和CListElmt中的成员的接口。

以上这些宏的运行时复杂度都为O(1),因为访问和检测数据结构的成员是一种简单的操作,而且它们都能在恒定的时间内完成。


11、使用循环链表的例子:第二次机会页面置换法

从前面的例子中我们已经知道在支持虚拟内存的系统中如何使用单链表管理内存页帧的分配。但还有一个问题没有搞清楚,那就是当空闲页面链表为空时,系统如何为其分配新的页帧呢?为了解决这个问题,操作系统从物理内存中取出一个页面将其放入称为交换磁盘的磁盘空间中,以这种方式来释放页帧。操作系统采用页面置换算法来决定哪一个页帧在当前时刻最适合释放。页面置换算法中的一个例子是第二次机会置换法,有时也称为时钟算法。

理想情况下,如果进程的所有页面都驻留在物理内存中那就太好了,但通常这是不可能的。一般情况下,有很多进程可能在系统中同时运行,所有的进程都在争用物理内存资源。有些时候,甚至一个单独的进程就需要占用非常大的地址空间,以至于它无法完全载入物理内存中。某些时候我们不得不面对必须去替换某些页面到交换磁盘的情况,那么,最适合操作系统替换的页面应该就是在未来最长的一段时间内都不会再次访问的那些页面,这才是最合理的。然而,由于我们无法预测未来,因此操作系统有时候会利用过去的表现作为一种对未来情况的合理假设,以此为依据去替换最近最少访问的那些页面。这种方法称为最近最少使用算法(Least Recently Used),或者LRU页面替换算法。

第二次机会置换法是实现LRU页面置换法的一种方式。它的工作方式是:维护一个当前存在于物理内存中的页面的循环链表。为了简化说明,假设链表中的每个元素只存储一个页码和一个引用值,引用值要么为1要么为0。在实践中,每个元素还会包含其他的信息。所有的页面初始的引用值都设置为0。每当系统访问页面时(比如,某个进程开始读或写某个页面),该页面的引用值就设置为1。

当需要某个页帧时,操作系统就使用它维护的循环链表以及引用值来判断哪些页面应该释放其页帧。为了确定这一点,开始遍历链表直到找到一个引用值为0的元素。当遍历每一个页面时,操作系统将页面的引用值从1重设回0。一旦它遇到引用值为0的元素,它就找到了一个自从上次遍历链表以来都没有被系统访问过的页面,因此这个页面就是最近最少使用的页面。那么这个页面就在物理内存中和新的页面置换,新的页面被插入链表中原来页面的位置。如果自从算法上次运行以来,所有的页面都被访问过了,那么操作系统就完整地遍历了一次链表,此时就置换它开始的页面。

下面的例子是关于这种页面置换策略的实现。这里实现了函数replace_page。这个函数接受唯一的参数current,current指向循环链表中的一个元素,该元素包含要开始搜索的页面(见下图)。当开始遍历链表时,该算法开始检测数据结构Page中存储的reference成员,检查其值为1还是0。如果是1,则重置该值为0,并遍历下一个页面;如果值为0,则找到了应该被替换的页面。最终,如果所有页面都被遍历过一次,根据循环链表的特性,算法将再次回到它刚开始遍历的页面。这时该页面的引用值应该为0(因为如果第一次遍历时遇到该页面,则其引用值应该已经重置为0),因此该页面就应该是待置换的页面。当函数返回时,current指向的页面就是搜索结束的位置。这个页面就是下一次当系统需要某个页帧时应该选择的页面。循环链表的模型非常完美地解决了这个问题,因为它允许操作系统循环遍历所有的页面,而这正是算法所需要的。replace_page函数的运行时复杂度是O(n),这里n代表循环链表中的页面数量。这是因为在最坏情况下,该算法有可能需要遍历整个链表才能找到应该被置换的页面。

第二次机会页面置换算法

// 第二次机会页面置换算法的头文件

/* page.h */

#ifndef PAGE_H
#define PAGE_H

#include "clist.h"

/* Define a structure for information about pages. */
typedef struct Page_
{
    int number;
    int reference;
} Page;

/* Public Interface */
int replace_page(CListElmt **current);

#endif // PAGE_H

// 第二次机会页面置换算法的实现

/* page.c */

#include "clist.h"
#include "page.h"

/* replace_page */
int replace_page(CListElmt **current)
{
    /* Circle through the list of pages until one is found to replace. */
    while(((Page *)(*current)->data)->reference != 0)
    {
        ((Page *)(*current)->data)->reference = 0;
        *current = clist_next(*current);
    }

    return ((Page *)(*current)->data)->number;
}



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值