表 (List ADT):
我们把一连串数字的排列这种数据类型称为表。比如:A1,A2,A3,A4…An这一组数,我们可以称它为大小为n的表。大小为0的表我们称之为空表(empty lists)。
对于一般的表,我们称An为An-1的后继元(successor),An为An+1的前驱元(predecessor)。
其对应操作有:
CreatList
Insert
Delete
Find
IsEmpty
PrintList
DeleteList
对表的所有操作都可以通过数组来实现。数组在内存分配上来看是连续储存的。如下图这个大小为9的数组(左边数字为其下标,右边数组是内存地址)。
虽说数组是动态指定的,但是还是需要对表的大小的最大值进行估计。为了防止溢出我们通常会估计得比较大,这样一来会浪费大量的空间。所以在表的大小未知或不确定的情况下,用数组的来实现表是一个比较局限的操作。
由于数组的连续储存特性:数组实现使得PrintList和Find操作以线性时间来执行。而在指定位置Insert和Delete花费时间也是线性的,这么看来不是很划算。
由于删除和插入的时间如此昂贵且要求表的大小提前预知,所以我们通常不使用简单数组来实现表这种结构。
链表:物理上非连续、非顺序的储存结构,数据元素之间的顺序是通过每个元素的指针关联的。
链表可以大致分为:单向链表、双向链表(也可以为他们加上循环特性)。
本文都是单向链表的总结,双向链表可以戳链接。
为了避免插入和删除的时间开销,我们允许表可以不连续储存。如下图,链表就很好地实现了这一想法。
下面给出链表的实现代码,采用分块的形式,在实际使用中其实不一定要固定这样写,怎么方便怎么来。只是各种操作的思想本质都包含在内了,自己总结一套方法(如果觉得我的行也很OK),灵活使用就好。
链表由一系列不必在内存中相连的结构组成,我们称之为链表的节点。每一个节点包至少含有表数据(数据域)、指向后继元的指针,通常称为next指针(指针域)。最后一个单元的next显然应该指向end,即NULL。
typedef struct Node {
int data; //储存表中数据
struct Node* next; //指向下一个节点的结构体指针
} *NODE, node;
NODE CreatList() {
NODE Head = (NODE)malloc(sizeof(node)); //申请一个节点作为表头
Head->next= NULL;
return Head;
}
返回值为head(结构体指针)。head通常做为标志节点,实际上不存储数据,仅仅作为表头,方便之后遍历整个表。可以看到,很多对表的操作都需要传入表,但是一般都是传入表头指针。
可以看到,对链表实行插入操作仅耗费常数时间。且链表的大小是动态变化的。
传入参数为待插入数据x,还有表头Head。
void Insert(int x, NODE Head) {
NODE temp = (NODE)malloc(sizeof(node)); //申请一个待插入节点
temp->data =x; //在节点中存入数据
temp->next = Head->next; //将表头后的节点连在待插入节点后
Head->next = temp; //将待插入节点连在表头后
}
将数据插入表尾
void Insert(int x, NODE Head) {
NODE temp = (NODE) malloc(sizeof(node)); //申请一个待插入节点
temp->data = x; //在节点中存入数据
temp->next = NULL;
NODE p = Head;
//找到表尾
while (p->next)
p = p->next;
p->next = temp; //将待插入节点连入表尾上
}
删除一个节点q,本质就是把q的前驱元和后继元连上。为了无效的内存访问,一定要考虑到删除数据是否在表内的情况。
传入参数为待删除数据x,还有表头Head。
void Delete(int x, NODE Head) {
NODE p = Head;
//找到待删除的元素
while (p->next && p->next->data != x)
p = p->next;
//找到待删除节点,p为其前驱元
if(p->next) {
NODE temp = p->next; //待删除节点
p->next = temp->next; //前驱元和后继元连上
free(temp); //释放待删除节点,防止内存泄漏
}
}
传入参数为待查找数据x,还有表头Head。
NODE Find(int x, NODE Head) {
NODE p = Head->next;
//找到待查找的元素
while (p && p->data != x)
p = p->next;
//返回x所在节点
return p; //若没有找到x则会返回NULL
}
int IsEmpty(NODE Head) {
//如果表头是空则为空表
return Head->next == NULL;
}
void PrintList(NODE Head) {
NODE p = Head->next;
while (p) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
void DeleteList(NODE Head) {
NODE p = Head;
while (p) {
NODE temp = p->next;
free(p);
p = temp;
}
}
按照平常码题经常出现bug的地方总结一下:
1、遍历链表时一定要注意当前的操作节点是否为NULL,对NULL的任何操作都很容易RE(无效内存访问)。
2、对于数据比较多的表,在每次申请节点(即malloc操作后)最好还是判断一下是否申请成功。否则容易造成内存溢出。
NODE temp = (NODE)malloc(sizeof(node)); //申请一个待插入节点
if(!temp) //内存申请失败
FatalError("Out of space!");
3、养成好的编程习惯,对于无用的表元素,即时释放其内存空间,即free操作。