链表是很久之前学的东西了,没想到大一下学期的计算导论课又上到了这部分。回想起来,脑袋早已空空(bushi)。为了重温一遍代码,特地在这里写篇博客。
链表的概念
线性表是一种很常用的数据结构,又分为顺序表和链表。其中,顺序表可以简单理解为“数组”这个概念,这里我们讲解链表的概念。
按正常方式定义一个数组的时候,计算机会从内存中取出一块连续的地址来存放给定长度的数组;而链表则使由若干个结点组成(每个结点代表一个元素),且结点在内存中的存储位置通常是不连续的。此外,链表的两个结点之间一般通过一个指针来从一个结点指向另一个结点。
因此,链表的结点一般又两部分构成,即数据域和指针域。
struct node {
typename data;
node* next;
};
一般来说,数据域存放结点要存储的数据,而指针域指向下一个结点的地址,这样就可以产生一个从某个结点开始的、由指针连接的一条链式结构,即链表。
而以链表是否存在头结点,又可以把链表分为带头结点和不带头结点的链表。头结点一般称为head,且其数据域data不存放任何内容,而指针域next指向第一个数据域有内容的结点。我们一般采用后者,这样做是为了方便链表的修改操作。最后一个结点的next指向NULL,即空地址,表示一条链表的结尾。
使用malloc函数或new运算符为链表分配内存空间
1. malloc函数
malloc函数是C语言stdlib.h文件头下用于申请动态内存的函数,其返回类型是申请的同变量类型的指针,其基本用法如下:
typename* p = (typename*)malloc(sizeof(typename));
//for example
int* p = (int*)malloc(sizeof(int));
node* p = (node*)malloc(sizeof(node));
2. new运算符
new是C++中用来申请动态空间的运算符,其返回类型同样是申请的同变量类型的指针。基本用法如下:
typename* p = new typename;
int* p = new int;
node* p = new node;
内存泄露
1. free()函数
free(p);
对应malloc,释放指针变量p所指向的内存空间,并将指针变量p指向空地址NULL。在free函数执行后,指针变量p并没有消失,但指向的内存被释放。
2. delete运算符
delete(p);
对应new运算符,作用和free函数一样。
一般在考试中,即使不释放空间,也不会产生很大影响。但在编程习惯上,应该养成及时释放空间的好习惯。
链表的基本操作
1. 创建链表
#include "stdio.h"
#include "stdlib.h"
int n, Array[10000];
struct node { //链表结点
int data;
node *next;
};
//创建链表(关键函数)
node *create(int Array[], int n) {
node *p, *pre, *head; //pre保存当前结点的前驱结点,head是头结点
head = new node; //创建头结点
head->next = NULL; //头结点不需要数据域,指针域初始为NULL
pre = head; //记录pre为head
for (int i = 0; i < n; ++i) {
p = new node; //新建结点
//将Array[i]赋给新建的结点作为数据域,也可以scanf输入
p->data = Array[i];
p->next = NULL; //新结点的指针域设为NULL
pre->next = p; //前驱结点的指针域设为当前结点的地址
pre = p; //把当前结点赋值给pre,作为下一个结点的前驱结点
}
return head; //返回头结点指针
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
scanf("%d", &Array[i]);
}
node *L = create(Array, n); //L是头结点
L = L->next; //从第一个结点开始才有数据域
while (L != NULL) {
printf("%d ", L->data);
L = L->next;
}
return 0;
}
/*
* input:
* 5
* 5 4 3 2 1
* output:
* 5 4 3 2 1
*/
2. 查找元素
//在以head为头结点的链表上计数元素x的个数
int search(node *head, int x) {
int cnt = 0; //计数器
node *p = head->next; //从第一个结点开始
while (p != NULL) { //只要没到链表末尾
if (p->data == x) {
cnt++; //当前结点数据域为x,则cnt++
}
p = p->next; //指针移动到下一个结点
}
return cnt; //返回计数器cnt
}
上面这部分代码可以直接写在“创建链表”部分的代码中进行使用,把create函数返回的头指针L直接作为第一个参数传入即可。
3. 插入元素
//将x插入以head为头结点的链表的第pos位置上
void insert(node *head, int pos, int x) {
node *p = head;
for (int i = 0; i < pos - 1; ++i) {
p = p->next; //为了跑到插入位置的前一个结点
}
node *q = new node; //新建结点
q->data = x;
q->next = p->next;
p->next = q;
}
4. 删除元素
// 删除以head为头结点的链表中所有数据域为x的结点
void del(node *head, int x) {
node *p = head->next;
node *pre = head;
while (p != NULL) {
if (p->data == x) { //数据域恰好为x,说明要删除该结点
pre->next = p->next; //上一个结点指向这个结点的下一个结点
delete (p); //删除这个结点
p = pre->next; //让这个结点指向下一个结点,继续寻找
} else { //数据域不是x,把pre和p都后移一位
pre = p;
p = p->next;
}
}
}
静态链表
如果结点的地址都是比较小的整数,就没有必要去建立静态链表,而应该使用方便得多的静态链表、
静态链表的实现原理的hash,即通过建立一个结构体数组,并令数组的下标直接表示结点的地址,来达到直接访问数组中的元素就能访问结点的效果。另外,由于结点的访问非常方便,因此静态链表不需要头结点。
定义方法如下:
struct Node { //链表结点
typename data; //数据域
int next; //指针域
}node[size];
在使用静态链表时,尽量不要把 结构体类型名和结构体变量名取成相同的名字,防止在sort操作中编译出错。