7.3 链表处理
7.3.1 链表的概念
线性表是一类很常用的数据结构,分为顺序表和链表。其中顺序表可以简单理解为数组。
按正常方式定义一个数组时,计算机会从内存中取出一块连续的地址来存放给定长度的数组;而链表则是由若干个节点组成(每个节点代表一个元素),且节点在内存中的位置通常是不连续的。除此之外,链表的两个节点之间一般通过一个指针来从一个节点指向下一个节点,因此链表的节点一般由两部分构成,即数据域和指针域:
struct Node{
typename data ; //数据域
Node* next ; //指针域
} ;
以链表是否存在头节点,又可以把链表分为带头节点的链表和不带头节点的链表。头节点一般称为head,其数据域data不存放任何内容,而指针域next指向第一个数据域有内容的节点(即第一个节点)。
事实上链表在内存中的存储形式如下,一般是不连续的。
7.3.2 使用malloc函数或new运算符为链表节点分配内存空间
1. malloc函数
malloc函数是C语言中stdlib.h头文件下用于申请动态内存的函数,其返回类型是申请的同变量类型的指针,用法如下
typename *p = (typename*)malloc(sizeof(typename)) ;
以申请一个int型变量和一个Node型数据结构为例:
int* p1 = (int*)malloc(sizeof(int)) ;
Node* p2 = (Node*)malloc(sizeof(Node)) ;
这个写法的逻辑是:以需要申请的内存空间大小(即sizeof(Node))为malloc函数的参数,这样malloc函数就会向内存申请一块大小为sizeof(Node)的空间,并且返回指向这块空间的指针。但是此时这个指针是一个未确定类型的指针void* , 因此需要把它强制类型转换为Node型的指针,因此在malloc之前加上(Node)。
如果申请失败,则会返回空指针NULL。
失败一般发生在使用malloc申请了较大的动态数组,即:
int* p = (int*)malloc(100000 * sizeof(int)) ;
2. new运算符
new是C++中用来申请动态空间的运算符,其返回类型同样是申请的同变量类型的指针。
typename* p = new typename ;
如果申请失败,则会启动C++异常机制处理而不是返回空指针NULL。和malloc同理,如果使用new申请了较大的动态数组:
int* p = new int[10000000] ;
这是会发生异常。
3. 内存泄漏
内存泄漏是指使用了malloc与new开辟出来的内存空间在使用过后没有释放,导致其在程序结束之前始终占据该内存空间,这在一些较大的程序中很容易导致内存消耗过快以至最后无内存可分配。C/C++语言设计者认为,程序员完全有能力自己控制内存的分配与释放,因此把对内存的控制操作全部交给了程序员。因此,在使用了malloc与new开辟出来的空间后必须将其释放。
(1)free函数
free函数是对应malloc函数的。
//设需要释放的内存空间指针标量为P
free(p) ;
free函数主要实现了两个效果:
- 释放变量p所指向的内存空间。
- 将指针变量p指向空地址NULL。
(2)delete运算符
delete运算符是对应new运算符的,其使用方法和实现效果和free函数一样
delete(p) ;
7.3.3 链表的基本操作
1. 创建链表
头插法创建
头插法创建时,顺序输出链表的值为输入顺序的倒序。
#include<bits/stdc++.h>
using namespace std ;
struct Node{
int data ;
Node* next ;
}node;
//头插法创建链表
Node* headCreate(){
Node *p , *head ;
head = new Node ; //创建头节点
head->next = NULL ; //头节点不需要数据域,指针域初试为空
int temp ;
//输入不为EOF,则一直循环
while( cin >> temp ){
p = new Node ;
p->data = temp ;
p->next = head->next ;
head->next = p ;
}
return head ;
}
int main(){
Node* head = headCreate() ;
Node* p = head->next ;
while( p != NULL ){
cout << p->data <<endl ;
p = p->next ;
}
//若输入为2 5 1 8 9
//输出则为9 8 1 5 2
return 0 ;
}
尾插法创建
尾插法创建时,顺序输出链表与输入顺序一致。
#include<bits/stdc++.h>
using namespace std ;
struct Node{
int data ;
Node* next ;
}node;
Node* tail_Create(){
//p为新创建节点,pre为p前面一个节点,head为头节点
Node *p , *pre , *head ;
head = new Node ; //创建头节点
head->next = NULL ;
pre = head ;
int temp ;
while( cin >> temp ){
p = new Node ;
p->data = temp ;
p->next = NULL ;
pre->next = p ;
pre = p ;
}
return head ;
}
int main(){
Node* head = tail_Create() ;
Node* p = head->next ;
while( p != NULL ){
cout << p->data <<endl ;
p = p->next ;
}
return 0 ;
}
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 ;
}
else{
p = p->next ;
}
}
return count ;
}
3. 插入元素
元素4所在节点的指针域next指向元素6所在节点地址。
元素3所在节点的指针域next指向元素4所在节点地址。
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 ; //前一个位置的节点指向新节点
}
删除元素
删除是这样进行的:
-
由指针变量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->data == x ){
pre->next = p->next ;
delete(p) ;
p = pre->next ;
}
else{
pre = p ;
p = p->next ;
}
}
}
7.3.4 静态链表
对于有些问题来说,节点的地址是比较小的整数(例如5位数的地址),这样就没必要去建立动态链表,而应使用方便得多的静态链表。
静态链表的实现原理时Hash,即通过建立一个结构体数组,令数组下标直接表示节点的地址。静态链表不需要头节点。
struct Node{
typename data ; //数据域
int next ; //指针域
}node[size] ;