链表
list 链表是线性表的一类。线性表分顺序表和链表,顺序表可以简单理解成数组
正常定义一个数组,计算机会从内存中取出一块连续的地址来存放给定长度的数组
而链表则是由若干个结点组成,每个结点代表一个元素,且结点在内存中的存储位置通常是不连续的
链表一般由两部分组成,数据域和指针域
struct node {
typename data; //数据域 要存放的数据
node* next; //指针域 指向下一个结点的地址
}
以链表是否存在头结点,可以把链表分成带头结点的链表和不带头结点的链表
头结点和第一个结点:头结点一般称为head,且其数据域data不存放任何内容,指针域next指向第一个数据域有内容的结点(一般直接把这个结点叫做第一个结点)
最后一个结点的next指针指向NULL,即空地址,表示一条链表的结尾
下文链表均采用带头结点的写法
链表结点分配内存空间
介绍两种方法:C的malloc函数与C++的new运算符
- malloc函数
malloc函数在C中stdlib.h头文件下用于申请动态内存的函数,其返回类型是申请的同变量类型的指针
typename* p = (typename*)malloc(sizeof(typename));
//以申请一个int型变量和node型结构体变量为例
int* p = (int*)malloc(sizeof(int));
node* p = (node*)malloc(sizeof(node));
//以需要申请的内存空间大小(即sizeof(node))为malloc函数的参数
malloc函数向内存申请一块大小为sizeof(node)的空间,并且返回指向这块空间的指针。
至此该指针仍是一个未确定类型的指针void,需要把它强制转换为node型的指针,在malloc前加(node),于是等号右边得到一个node型的指针,并通过赋值等号把这个指针赋给node*型的指针变量p。
至此,成功申请了一块node类型大小的内存空间,并通过指针p来访问它。
可能存在失败的情况,就会返回空指针NULL
int* p = (int*)malloc(100000 * sizeof(int)); //申请了较大的动态内存
- new运算符
new是C++中用来申请动态空间的运算符,其返回类型同样是申请的同变量类型的指针
typename* p = new typename;
//以申请一个int型变量和node型结构体变量为例
int* p = new int;
node* p = new node;
//可以申请较大动态数组
int* p = new int[100000];
- 内存泄漏
内存泄漏是指使用malloc和new开辟出来的内存空间在使用过后没有释放,导致其在程序结束之前始终占据该内存空间。
需要及时释放malloc和new开辟出来的空间。
3.1 free
free函数对应malloc函数,在stdilb.h头文件下
实现两个效果:释放指针变量p所指向的内存空间,将
free(p); //p为所需要释放内存空间的指针变量
3.2 delete运算符
delete运算符对应new运算符,其使用方法和实现效果均与free相同
delete(p);
链表基本操作
- 创建链表
#include<stdio.h>
#include<stdlib.h>
struct node {
int data;
node* next;
};
node* create(int Array[]) {
node *p, *pre, *head; //pre保存当前结点的前驱结点,head为头结点
head = new node; //创建头结点
head->next = NULL; //头结点不需要数据域,指针域初始为NULL
pre = head; //记录pre为head
for(int i = 0; i < 5; i++) {
p = new node; //创建结点
//将Array[i]赋给新建的结点作为数据域,也可以scanf输入
p->data = Array[i];
p->next = NULL; //新结点的指针域需要设为NULL
pre->next = p; //前驱结点的指针域设为当前新建结点的地址
pre = p; //把pre设为p,作为下个结点的前驱
}
return head;
}
int main() {
int Array[5] = {5, 3, 6, 1, 2};
node* L = create(Array); //新建链表,返回的头指针head赋给L
L = L->next; //从第一个结点开始有数据域
while(L != NULL) {
printf("%d ", L->data); //输入每个结点的数据域
L = L->next;
}
return 0;
}
//输出 5 3 6 1 2
- 查找元素
从第一个结点开始,不断判断当前结点的数据域是否等于 x,如果等于,那么就给计数器count + 1。当链表到链表结尾时,count的值就是链表中元素 x 的个数。
int search(node* head, int x) {
int count = 0; //计数器
node* p = head->next; //从第一个结点开始
while(p != NULL) { //只要没有到链表末尾
if(p->data == x) {
count++;
}
p = p->next;
}
return count;
}
//该段代码也可直接写进创建链表中使用,将创建的头指针L作为第一个参数传入
- 插入元素
首先找到插入结点的位置,将要插入位置的结点指向后一个结点,再将插入位置的前一个结点指向需要插入的结点处(想想为什么不能调换顺序?初学的时候卡过我)
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; //新结点的数据域为x
q->next = p->next; //新结点的下一个结点指向原先插入位置的结点
p->next = q; //前一个位置的结点指向新结点
}
//该段代码同样可插入到创建链表的过程中
- 删除元素
首先由指针变量p枚举结点,另一个指针变量pre表示p指向结点的前驱结点;当p所指数据域恰好为x时,令pre所指结点的指针域next指向p所指结点的下一个结点,释放p所指结点的内存空间,最后令p指向pre所指结点的下一个结点
void del(node* head, int x) {
node* p = head->next; //p从第一个结点开始枚举
node* pre = head; //pre始终保存p的前驱结点的指针
while(p != NULL) {
if(p->next == x) { //找到数据域为x的结点
pre->next = p->next;
delete(p);
p = pre->next;
} else {
pre = p;
p = p->next;
}
}
}
//同样也可再创建链表部分的代码中使用
静态链表
前面讲的都是动态链表,即需要指针来建立结点之间的连接关系。而对有些问题如结点的地址是比较小的整数就没有必要去建立动态链表,而应使用方便的多的静态链表
静态链表的实现原理是hash,即通过建立一个结构体数组,并令数组的下标直接表示结点的地址,来达到直接访问数组中的元素就能访问结点的效果。另外,由于结点的访问非常方便,因此静态链表是不需要头结点的。
struct Node {
typename data; //数据域
int next; //指针域
}node[size];
next是一个int型的整数,用以存放下一个结点的地址(事实上就是数组下标)
node[11111] = 22222;
node[22222] = 33333;
node[33333] = -1; //-1对应动态链表的NULL,表示没有后继结点
在使用静态链表时,尽量不要把结构体类型名和结构体变量名取相同名字