《算法精解:C语言描述》note-2 链表
文章目录
- 2 链表
- 2.1 单链表
- 单链表介绍
- 单链表接口定义
- 单链表的实现
- 虚拟内存机制下的页帧管理
- 2.2 双向链表
- 双向链表介绍
- 双向链表的接口定义
- 双链表的实现
- 2.3 循环链表
- 循环链表介绍
- 单向循环链表接口定义
- 单向循环链表的实现
- 第二次机会页面置换算法
- 2.4 链表和数组的区别
《算法精解:C语言描述》这本书在讲解数据结构和算法的概念同时,使用C代码而不是伪代码来实现具体的细节,很适合刚学完C语言的人来读:既可以熟练各种用法,也能了解数据结构和算法的底层实现。
这里是链表的接口和实现。
2 链表
可分为单链表、双向链表、循环链表。
链表的应用:
- 邮件列表。电子邮件程序中常用到。
- 滚动列表,图形用户界面的滚动列表的条目相关的数据不直接显示,而是通过链表保存。
- 多项式计算,可以用于表示多项式。
- 内存管理,跟踪可供分配的内存片段的信息。
- LISP语言,进行符号处理时大量使用链表。
- 文件的链式分配,一种文件分配方式,每个块包含指向文件下一块数据的指针,只适合顺序访问,可以消除磁盘上的碎片。
- 其他数据结构的视线依赖于链表。
2.1 单链表
单链表介绍
每个元素包含两部分,数据内容和一个称为next的指针。每个元素的next指针指向下一个的元素。
单链表只能从头到尾以一个方向进行遍历。想访问当前位置之前的元素必须重新遍历。
单链表接口定义
void list_init(List *list, void (*destroy)(void *data));
初始化链表。
参数destroy是释放动态分配的数据的方法,链表被销毁时用到。void list_destroy(List *list);
销毁链表。该链表初始化时输入的destroy函数会在每个元素移除时都调用一次。int list_ins_next(List *list, ListElmt *element, const void *data);
在链表指定元素的后面插入一个新元素,元素的数据为data
。
element为NULL时插入到头部。int list_rem_next(List *list, ListElmt *element, void **data);
移除链表指定元素后面的一个元素。element为NULL时删除第一个元素。单向链表没有指向上一个元素的指针,如果直接删除指定的元素,就没办法更改前一个元素的next指针。
如果先把下一个元素的数据放到当前元素的数据上,再删除下一个元素,就实现了删除当前元素数据的操作。但是这么做产生了额外的影响:使下一个元素的指针失效。int list_size(const List *list);
计算指定链表的元素个数。ListElmt *list_head(const List *list);
返回指向头元素的指针。ListElmt *list_tail(const List *list);
返回指向尾元素的指针。int list_is_head(const ListElmt *element);
判断指定元素是否为链表的头结点。int list_is_tail(const ListElmt *element);
判断指定元素是否为链表尾节点。void *list_data(const ListElmt *element);
返回结点元素中保存的数据。ListElmt *list_next(const ListElmt *element);
返回指定结点的下一个结点。
list_destroy
的复杂度为O(n)
,其余的函数复杂度都是O(1)
。
单链表的实现
头文件list.h
。其中包含了公共的类型定义、宏定义和函数原型。
// 如果未定义LIST_H,就编译该文件的内容
#ifndef LIST_H
// 定义一个LIST_H宏,用于标识该头文件
#define LIST_H#include <stdlib.h>// 定义链表元素的结构
typedef struct ListElmt_ {// 指向数据的指针void *data;// 指向下一个元素的指针struct ListElmt_ *next;
} ListElmt;// 定义链表的结构
typedef struct List_ {int size;int (*match)(const void *key1, const void *key2);void (*destroy)(void *data);// 指向首尾元素的指针ListElmt *head;ListElmt *tail;
} List;// 公共接口
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(lsit, element) ((element) == (lsit)->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
上面头文件中的几处用法解释:
- 避免多次包含
头文件(.h
结尾的文件)中包含类型定义时,如果某个代码使用#include
指令多次包含这个头文件,编译时会发生错误。上面开头的#ifndef
和末尾的#endif
指令,就是为了避免在嵌套的程序结构中使用#include
指令时多次包含同一个头文件的情况。 - 函数式宏
链表大小、获取和判断首尾元素、获取数据、获取下一个元素的指针,这几个功能写函数过于简单,可以使用函数式宏实现:#define 函数名(函数参数...) 替换值
。
使用函数式宏时,参数在替换值中出现时最好都加上圆括号,这样当参数为表达式时,替换后也能确保正确的优先级和结合性。 - 类型的成员
在类型中定义size
、head
、tail
等成员,并在后续操作中维护它们。可以使获取链表大小或首尾元素的复杂度为O(1)
而不是O(n)
,也不会为插入和移除操作造成额外的开销。
list.c
文件包含具体的函数定义。
#include <stdlib.h>
#include <string.h>// 尖括号表示在系统的头文件目录搜索文件
// 双引号表示在当前目录搜索该文件
#include "list.h"// 初始化
void list_init(List *list, void (*destroy)(void *data)) {list->size = 0;list->destroy = destroy;list->head = NULL;list->tail = NULL;return;
}// 销毁
void list_destroy(List *list) {void *data;while (list_size(list) > 0) {// 从第一个开始删除元素if (list_rem_next(list, NULL, (void **)&data) == 0 && list->destroy != NULL) {// 每个成功删除的元素都清理内存list->destroy(data);}}memset(list, 0, sizeof(List));return ;
}// 向链表list的指定元素element后插入新元素,数据为data
int list_ins_next(List *list, ListElmt *element, const void *data){ListElmt *new_element;// 分配内存给新元素if ((new_element = (ListElmt *)malloc(sizeof(ListElmt))) == NULL)return -1;new_element->data = (void *)data;if (element == NULL) {// 指定元素指定为NULL,则新元素插入到首位if (list_size(list) == 0)list->tail = new_element;new_element->next = list->head;list->head = new_element;} else {// 指定元素为链尾if (element->next == NULL)list->tail = new_element;new_element->next = element->next;element->next = new_element;}list->size++;return 0;
}// 移除list中元素element之后的元素
int list_rem_next(List *list, ListElmt *element, void **data) {// 记录旧元素,用于释放内存ListElmt *old_element;// 空列表无法操作if (list_size(list) == 0)return -1;if (element == NULL) {// 指定元素为NULL,移除首位元素*data = list->head->data;old_element = list->head;list->head = list->head->next;if (list_size(list) == 1)list->tail = NULL;} else {// 指定尾部元素,无法删除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(old_element);lsit->size--;return 0;
}
虚拟内存机制下的页帧管理
虚拟内存是计算机内存管理的核心机制。
在该机制下,硬盘划出的虚拟内存空间和实际的物理内存被分割为固定大小的页。操作系统的内存管理单元维护着一个页表,其中记录虚拟内存地址到物理内存地址的映射。
程序启动时仅加载必要的页到物理内存中,其它页在首次访问时动态加载。当运行的程序需要访问这些页时,首先访问到的是虚拟地址,其格式和物理内存的地址一样。然后由操作系统通过页表将这个虚拟地址转换为物理内存地址,程序才能访问到数据。
如果访问的页不在物理内存中,操作系统就从磁盘上加载这些页。物理内存不足时,操作系统又会把不活跃的页交换到虚拟内存区域。
可以使用链表来维护空闲的物理页帧,页帧的插入和删除都很方便。
下面两个函数,aloc_frame
在需要加载数据到物理内存时从空闲页帧链表中取空闲页帧号,free_frame
在物理内存释放数据时把页帧号加入空闲页帧链表中。
#include <stdlib.h>#include "frames.h"
#include "list.h"int alloc_frame(List *frames) {int frame_number, *data;// 如果没有空闲页可用if (list_size(frames) == 0)return -1;else {// 前面判定时有可用页,取的时候没有了if (list_rem_next(frames, NULL, (void **)&data) != 0)return -1;else {// 该页地址frame_number = *data;// 确保该页清空free(data);}}return frame_number;
}int free_frame(List *frames, int frame_number) {int *data;// 为页帧地址分配存储空间if ((data = (int *)malloc(sizeof(int))) == NULL)return -1;// 将页帧地址加到空闲页帧链表*data = frame_number;// 该页帧地址无法插入空闲页帧链表if (list_ins_next(frames, NULL, data) != 0)return -1;return 0;
}
2.2 双向链表
双向链表介绍
双向链表的元素之间由两个指针链接。每个元素有3个组成部分:数据内容、next指针和一个指向前一个元素的prev指针。
双向链表的头部元素的prev指针和尾部元素的next指针都是指向NULL。
双向链表可以从尾到头进行遍历。
双向链表的接口定义
和单链表一样的接口:
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_size(const DList *list);
DListElmt *dlist_head(const DList *list);
DListElmt *dlist_tail(const DList *list);
int dlist_is_head(const DListElmt *element);
int dlist_is_tail(const DListElmt *element);
void *dlsit_data(const DListElmt *element);
DListElmt *dlist_next(const DListElmt *element);
双链表特定的接口:
int dlist_ins_prev(DList *list, DListElmt *element, const void *data);
把元素插入双向链表指定的元素之前。int dlist_remove(DList *list, DListElmt *element, void **data);
从双向链表移除指定元素。DListElmt *dlist_prev(const DListElmt *element);
返回指定元素的前驱元素。
dlist_destroy
的复杂度是O(n)
,其余都是O(1)
。
双链表的实现
双向链表类型实现的头文件dlist.h
:
#ifndef DLIST_H
#define DLIST_H#include <stdlib.h>// 双向链表的元素
typedef struct DListElmt_ {void *data;struct DListElmt_ *prev;struct DListElmt_ *next;
} DListElmt;// 双向链表的类型
typedef struct DList_ {int size;int (*match)(const void *key1, const void *key2);void (*destroy)(void *data);DListElmt *head;DListElmt *tail;
} DList;// 需在源文件实现的公共接口
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) ((lsit)->size)
#define dlsit_head(list) ((list)->head)
#define dlist_tail(list) ((lsit)->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
双向链表和单链表的类型定义和公共接口基本一致,只是添加了指向前一个元素的指针。
二者在具体实现上的区别也是对prev指针的管理。
双向链表类型实现的源文件dlist.c
:
#include <stdlib.h>
#include <string.h>#include "dlsit.h"void dlist_init(DList *list, void (*destroy)(void *data)) {list->size = 0;list->destroy = destroy;list->head = NULL;list->next = NULL;return ;
}void dlist_destroy(DList *list) {void *data;while (dlist_size(list) > 0) {if (dlist_remove(list, dlist_tail(list), (void **)&data) == 0 && list->destroy != NULL) {list->destroy(data);}}meset(lsit, 0, sizeof(DList));return;
}// 向指定元素element后插入新元素,新元素数据为data
int dlist_ins_next(DList *list, DListElmt *element, const void *data) {DListElmt *new_element;// 链表为空时才允许指定元素为NULL,新元素插入首位if (element == NULL && dlist_size(list) != 0)return -1;if ((new_element = (DListElmt *)malloc(sizeof(DListElmt))) == NULL)return -1;new_element->data = (void *)data;if (dlist_size(lsit) == 0) {// 链表为空list->head = new_element;list->tail = new_element;list->head->prev = NULL;list->head->next = NULL;} else {if (element->next == NULL)lsit->tail = new_element;elseelement->next->prev = new_element;element->next = new_element;}list->size++;return 0;
}// 向指定元素element前插入新元素,新元素数据为data
int dlist_ins_prev(DList *list, DListElmt *element, const void *data) {DListElmt *new_element;// 也是链表为空时才允许指定元素为NULL,新元素插入首位if (element == NULL && dlist_size(list) != 0)return -1;if ((new_element = (DListElmt *)malloc(sizeof(DListElmt))) == NULL)return -1;new_element->data = (void *)data;if (dlist_size(lsit) == 0) {list->head = new_element;list->tail = new_element;list->head->prev = NULL;list->head->next = NULL;} else {// 若指定元素为链首if (element->prev == NULL)lsit->head = new_element;elseelement->prev->next = new_element;element->prev = new_element;}list->size++;return 0;
}// 移除指定元素
int dlist_remove(DList *list, DListElmt *element, void **data) {// 指定元素不可为NULL,链表不能为空if (element == NULL || dlsit_size(list) == 0)return -1;*data = element->data;if (element == list->head) {// 若元素为头元素,就把下一个元素做为新的头元素list->head = element->next;if (lsit->head == NULL)lsit->tail=NULL;elseelement->next->prev = NULL;} else {// 元素不为头元素的情况element->prev->next = element->next;if (element->next == NULL)list->tail = element->prev;elseelement->next->prev = element->prev;}free(element);list->size--;return 0;
}
2.3 循环链表
循环链表介绍
循环链表可以是单向或双向链表。
单向循环链表的最后一个元素的next指针指向头元素而不是NULL。双向循环链表的的头元素的prev指针指向最后一个元素。
单向循环链表接口定义
单向循环链表的接口和单向链表一样:
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);
int clist_size(const CList *list);
CListElmt *clist_head(const CList *list);
void *clist_data(const CListElmt *element);
CListElmt *clist_next(const CListElmt *element);
单向循环链表的实现
单向循环链表类型实现的头文件clist.h
:
#ifndef CLIST_H
#define CLIST_H#include <stdlib.h>typedef struct CListElmt_ {void *data;struct CListElmt_ *next;
} CListElmt;typedef struct CList_ {int size;int (*match)(const void *key1, const void *key2);void (*destroy)(void *data);CListElmt *head;
} CList;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.c
:
#include <stdlib.h>
#include <string.h>#include "clist.h"void clist_init(CList *list, void (*destroy)(void *data)) {list->size = 0;list->destroy = destroy;list->head = NULL;return ;
}void clist_destroy(CList *list) {void *data;while (clist_size(list) > 0) {if (clist_rem_next(list, list->head, (void **)&data) == 0 && list->destroy != NULL)list->destroy(data);}memset(list, 0, sizeof(CList));return ;
}int clist_ins_next(CList *list, CListElmt *element, const void *data) {CListElmt *new_element;if ((new_element = (CListElmt *)malloc(sizeof(CListElmt))) == NULL)return -1;new_element->data = (void *)data;if (clist_size(lsit) == 0) {// 链表为空时插入,新元素next指向自己new_element->next = new_element;list->head = new_element;} else {// 链表不为空new_element->next = element->next;element->next = new_element;}list->size++;return 0;
}int clist_rem_next(CList *list, CListElmt *element, void **data) {CListElmt *old_element;if (clist_size(list) == 0)return -1;*data = element->next->data;if (element->next == element) {// 删除仅剩的一个元素old_element = element->next;list->head = NULL;} else {// 删除某个元素old_element = element->next;element->next = element->next->next;if (old_element == clist_head(lsit))list->head = old_element->next;}free(old_element);list->size--;return 0;
}
第二次机会页面置换算法
在虚拟内存机制中,当空闲页面链表为空时,操作系统会从物理内存中取出一个页面将其放入磁盘的虚拟内存区域(称为交换区),释放的物理内存的页帧会加入空闲页面链表进行分配。
页面置换算法是判断将哪一个页面放入交换区的算法。
现实中一般无法把进程的所有页面都载入物理内存中,所以必须置换某些页面到交换区。
操作系统最好把未来很长一段时间都不会用到的页面置换出去。操作系统有时会利用过去的表现来对未来做合理的预测,把最近最少使用的页面替换到交换区。
第二次机会置换法是最近最少使用算法的一种实现。
它会维护一个存在于内存中的页面的循环链表。循环链表的元素只存储一个页码和一个引用值,引用值为0或1。
所有页面初始的引用值为0,当页面被访问时其引用值就变为1。当需要置换页帧时,操作系统就遍历这个循环链表,把遍历到的每个元素的引用值从1设置为0。遇到引用值为0的元素时,该元素是从上一次遍历链表以来都没被系统访问过的页面,因此可以将其置换。全部都为1时,则会置换这次遍历开始的页面。
#ifndef PAGE_H
#define PAGE_H// 记录页信息的结构
typedef struct Page_ {int number;int reference;
} Page;int replace_page(CListElmt **current);#endif
#include "clist.h"
#include "page.h"int replace_page(CListElmt **current){// 非0的置为0while (((Page *)(*current)->data)->reference != 0) {((Page *)(*current)->data)->reference = 0;*current = client_next(*current);}// 返回页面的页码return ((Page *)(*current)->data)->number;
}
2.4 链表和数组的区别
数组在内存中是连续排列的,所以任何元素都能在O(1)
的时间内通过索引访问。
数组中没有指针使每个元素链接起来,因此比链表更节约空间。
数组的插入、删除都是O(n)
级别的操作,因为每次变动都需要其他元素也跟着移位。
需要频繁插入和删除时,链表比数组更快。
链表的插入和删除操作需要先得到某个特定元素的指针,这一点最坏的情况下也需要遍历整个链表,使复杂度达到O(n)
。因此想利用链表高性能插入、删除的特点,必须和实际应用的特点结合进行设计。