单链表
用代码定义一个单链表:
- 定义一个结点类型
typedef struct Node //结点
{
ElemType data; //数据域
struct Node* next; //指针域
}Node;
2.定义一个单链表L
//表示一个单链表时,只声明一个头指针L,指向链表的第一个结点,通过这个指针就可以确定整条链表
//因此指向第一个结点的指针L,也可以用来指代这个单链表
typedef struct Node* Linklist; //这是一个指向结点的指针
增加一个新的结点:在内存中申请一个结点所需的空间,并用指针p指向这个结点
struct Node * p = (struct Node *)malloc(sizeof(struct Node));
// Node * L; //声明指向链表的第一个结点的指针L,这种方式强调这是一个结点
// LinkList L;// 声明指向链表的第一个结点的指针L ,两者是等价的,这个代码可读性更强一些,强调这是一个单链表
1. 初始化一个单链表
不带头结点的单链表
typedef struct Node{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素
struct Node *next;//指向下一个结点
}Node, *LinkList;
//初始化一个空的链表
bool InitList(Linklist & L){ //传入引用,修改链表本身而不是拷贝
//防止脏数据
L = NUll; //空表,暂时还没有任何结点
return true;
}
void test(){ //测试程序
//*******注意:此处并没有创建一个结点*********//
LinkList L; //声明一个指向单链表的指针
//初始化一个空表
InitList(L);
//....后续代码...
}
//判断单链表是否为空
bool Empty(LinkList L){
return (L == NULL);
}
带头结点的情况
typedef struct Node{
ElemType data;
struct Node *next;
}Node, *LinkList;
//初始化一个单链表(带头结点)
bool InitList(Linklist & L){
L = (Node *)malloc(sizeof(Node)); //分配一个头结点
if(L == NULL) //内存不足,分配失败
return false;
L->next = NULL; //头结点之后暂时还没有结点
return true;
}
void test(){
LinkList L; //声明一个指向单链表的指针
//初始化一个空表
InitList(L);
//....后续代码...
}
//判断单链表是否为空
bool Empty(LinkList L){
return (L->next == NULL); //
}
使用带头结点的单链表会更方便
总结
2. 单链表的插入和删除
插入
按位序插入(带头结点)
在第i个位置前插入元素e(带头结点)
bool ListInsert(LinkList &L, int i,Elemtype e){
if(i < 1) //位序i< 1不合法
return false;
Node *p; //指针p用来指向当前扫描到的结点
int j = 0; //用来记录指针p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
while (p != NULL && j < i - 1){ //循环找到第i-1个结点(在第i个位置前插入)
p = p->next; //p不断指向下一个结点
j++;
}
if (p == NULL) //位序i 太大了 会因为p != NULL条件不满足而跳出循环
return false;
Node *s = (Node *)malloc(sizeof(Nodee));
s->data = e;
//*******注意这两句的顺序不能颠倒********//
s->next = p->next;
p->next = s; //将s结点插入到p之后
return true; //插入成功
}
按位序插入(不带头结点)
在第i个位置前插入元素e(不带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
if(i < 1)
return false;
//****不带头结点需要特殊处理*******//
if (i == 1){
Node * s = (Node *)malloc(sizeof(Node));
s->data = e;
s->next = L;
L = s;
return ture;
}
Node * p; //指向当前结点
int j = 1; //记录p指向第几个结点
p = L; //p指向第一个结点(注意不是头结点)
while(p != NULL && j < i - 1){
p = p->next;
j++;
}
if(p == NULL)
return false; //i过大超出范围,是的p = NULL跳出循环
Node *s = (Node * )malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return ture;
}
指定结点的后插操作
后插操作:在p结点之后插入元素e
bool InsertNextNode(Node *p, ElemType e){
if (p == NULL)
return false;
Node *s = (Node *)malloc((sizeof(Node)));
if (S == NULL) //内存分配失败
return false;
s->data = e;吧 //用结点s保存数据e
s->next = p->next;//将结点s连到结点p之后
p->next = s;
return turel;
}
在第i个位置插入e可以通过调用 在p结点之后插入元素e来实现
bool ListInsert(LinkList &L, int i,Elemtype e){
//*******找到第i-1个结点p********//
if(i < 1) //位序i< 1不合法
return false;
Node *p; //指针p用来指向当前扫描到的结点
int j = 0; //用来记录指针p指向的是第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
while (p != NULL && j < i - 1){ //循环找到第i-1个结点(在第i个位置前插入)
p = p->next; //p不断指向下一个结点
j++;
}
//在p结点后面插入元素e
return InsertNextNode(p, e);
}
指定结点的前插操作
指定p结点时,由于只有后继信息,因此不能找到p前面一个结点进行插入
于是有两种方案
方案一:引入头指针L,通过遍历链表的方式,找到p结点的前驱结点,时间复杂度为O(n)
方案二:极限换家骚操作,在p结点后面插入结点s,将p的内容转移到s,再将结点p换成待插入的内容e,时间复杂度为O(1)
删除
按位序删除(带头结点)
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
最坏、平均时间复杂度:O(n)
最好时间复杂度: O(1)
bool ListDelete(LinkList &L, int i, ElemType &e){
if(i < 1) //i值过小
return false;
Node *p; //指针p指向当前扫描到的结点
int j = 1;//当前p指向第几个结点
p = L; //L指向头结点,头结点是第0个结点(不存数据)
while(p != NULL && j < i - 1){ //循环找到第 i-1 个结点
p = p->next;
j++;
} //退出循环时p找到第i-1个结点
if (p == NULL) //i值过大
return false;
Node *q = p->next; //令q指向被删除结点
e = q->data; //用e返回元素的值
p-next = q->next; //令*q结点从链中断开
free(q) //释放结点的存储空间
return ture; //删除成功
}
指定结点的删除
这里也是一种换家策略,时间复杂度O(1)
bool DeleteNode(Node * p){
if (p == NULL)
return false;
Node * q = p->next; //令q指向*p的后继节点
p->data = q->data; //将后继节点交换收据保存到p结点中
p->next = q->next; //将*q结点从链中断开
free(q); //释放后继节点*q的存储空间
return ture;
}
但是如果p结点是最后一个结点, p->data = q->data;会产生错误,需要特殊处理,只能从表头依次寻找p的前驱,时间复杂度O(n)
单链表的局限性:无法逆向检索,有时候不太方便
3. 单链表的查找 (本节只探讨带头结点的情况)
按位查找
GetElem(L,i):按位查找操作,获取表中第i个位置的元素的值
按位查找,返回第i个元素(带头结点),平均时间复杂度O(n)
Node * GetElem(LinkList L,int i){
if (i < 0)
return NULL;
Node * p; //p指向当前结点
int j; //记录P指向第几个节点
p = L; //L指向头结点,头结点是第0个结点(不存在数据)
while (p != NULL && p < i){
p = p->next;
j++;
}
return p; // 当i值不合法时,返回值p是NULL
}
按值查找
LocateElem(L,e):按值查找操作,在表中查找具有给定关键字值的元素
按值查找,找到数据域==e 的结点,时间复杂度0(n)
Node * LocateElem(LinkList L,ElemType e){
Node * p = L->next; //从第一个结点开始查找数据域为e的结点
while(p != NULL && p->data != e){
p = p->next;
}
return p; //找到后返回该结点的指针,否则返回NULL
}
求表的长度
时间复杂度O(n)
int Length(LinkList L){
int len = 0; //统计表长
Node * p = L;
while (p->next != NULL){
p = p->next;
len++;
}
return len;
}
总结
4. 单链表的整表建立和整表删除(本节讨论带头结点的情况)
创建:
把很多个数据元素(ElemType)如何存到一个单链表中?
- step1: 初始化一个单链表
- step2: 每次取一个数据元素,插入到表尾/表头
尾插法建立单链表
基本思路:
1. 初始化指向当前结点的指针p和尾结点指针r
2. 初始化计数器变量i
3. 初始化带头结点的单链表L
4. 循环{
生成一个新结点赋值给p
随机生成一数字赋值给p的数据域p->data;
将p结点插入到尾结点后面并更新尾指针
}
在第i个位置插入元素e(带头结点)
//随机数输入板
bool CreateListTail(LinkList & L,int n){
Node * p; //p指向当前待插入结点
Node * r; //r指向尾结点
int i;
srand(time(0));
L = (Node *)malloc(sizeof(Node));
if (L == NULL)
return false;
L->next = NULL;
r = L;
for(i = 0; i < n; i++){
p = (Node *)malloc(sizeof(Node));
p->data = rand()%100 + 1; //随机生成100以内的数字
r->next = p; //将新结点p接到当前表尾
r = p; //更新尾指针
}
r->next = NULL;
return ture;
}
//手动输入板
bool CreateListTail(LinkList & L){
Node * p; //p指向当前待插入结点
Node * r; //r指向尾结点
int e;
cout <<"请输入单链表数据,输入9999退出"<<endl;
cin >> e;
L = (Node *)malloc(sizeof(Node));
if (L == NULL)
return false;
L->next = NULL;
r = L;
while(e != 9999){
p = (Node *)malloc(sizeof(Node));
p->data = e;
r->next = p; //将新结点p接到当前表尾
r = p; //更新尾指针
cin >> e;
}
r->next = NULL;
return true;
}
头插法
L就是始终指向表头的指针
//随机数输入板
bool CreateListHead(LinkList & L,int n){
Node * p;
int i;
srand(time(0));
L = (Node *)malloc(sizeof(Node));
if (L == NULL)
return false;
L->next = NULL;
for(i = 0; i < n; i++){
p = (Node *)malloc(sizeof(Node));
p->data = rand() %100 + 1;
p->next = L->next; //插到表头
L->next = p;
}
return true;
}
//手动输入板
bool CreateListHead(LinkList & L){
Node * p;
int e;
cout <<"请输入单链表数据,输入9999退出"<<endl;
cin >> e;
L = (Node *)malloc(sizeof(Node));
if (L == NULL)
return false;
L->next = NULL;
while(e != 9999){
p = (Node *)malloc(sizeof(Node));
p->data = e;
p->next = L->next; //插到表头
L->next = p;
cin >> e;
}
return true;
}
头插法的重要应用:链表的逆置
//链表逆置:将传入的链表L逆置并返回一个新的链表
LinkList InversionList(const LinkList& L) {
LinkList temp;
Node* f = L->next; //指向L第一个结点
Node* p;
temp = (Node*)malloc(sizeof(Node));
temp->next = NULL;
while (f != NULL) {
p = (Node*)malloc(sizeof(Node));
p->data = f->data; //结点p接受链表L的数据
p->next = temp->next; //使用头插法将结点p插入链表temp
temp->next = p;
f = f->next; //链表L从前往后头
}
return temp;
}
总结:头插法、尾插法的核心就是初始化操作、指定节点的后插操作
注意:
- 尾插法注意设置一个指向尾结点的指针
- 头插法就是在头结点进行后插
删除
算法思路如下:
1.声明两个结点指针p和q
2.将第一个结点赋值给p
3.循环
(1)将p的下一个结点赋给q
(2) 释放p结点
(3) q赋值给p,p后移
bool ClearList(LinkList& L) {
LinkList p, q;
p = L->next;
while (p != NULL) {
q = p->next; //q用来记录p下一结点的位置
free(p); //释放p结点
p = q; //p指向下一个结点
}
L->next = NULL;
return true;
}
双链表
具有前驱指针和后继指针,拥有双向检索能力,使用起来更方便。
1. 双链表的初始化(带头结点)
//初始化双链表
typedef int ElemType;
typedef struct DNode {
ElemType data;
struct Dnode* prior, * next;
}DNode, * DLinkList;
//双链表初始化函数
bool InitDLinkList(DLinkList& L) {
L = (DNode*)malloc(sizeof(DNode)); //分配一个头结点
if (L == NULL) //内存不足,分配失败
return false;
L->prior = NULL; //头结点的prior永远指向NULL
L->next = NULL; //头结点之后暂时还没有分配结点
return true;
}
void test() {
DLinkList L;
InitDLinkList(L);
//后续代码
}
//******判断双链表是否为空(带头结点)*******//
//只需要判断头结点的next指针是否为空即可
bool Empty(DLinkList L){
if (L->next == NULL)
return true;
else
return false;
}
2. 双链表的插入
在p结点之后插入s结点,时间复杂度O(1)
//错误的示范
bool InsertNextDNode_Bad(DNode* p, DNode* s) {
s->next = p->next;
p->next->prior = s; //如果p结点恰好是最后一个结点,那么p->next == NULL,这句会有一个空指针的错误
p->next = s;
s->prior = p;
return true;
}
//正确示范:在p结点之后插入s结点
bool InsertNextDNode(DNode* p, DNode *s) {
if (p == NULL || s == NULL) //非法参数
return false;
s->next = p->next;
if (p->next != NULL) //如果p结点有后继节点,
p->next->prior = s; //执行原本p后继节点的前插,否则跳过这句,s结点就作为最后一个节点
p->next->prior = s;
p->next = s;
s->prior = p;
return true;
}
按位序插入(调用结点后插入)
//按位序插入
bool InsertDNode(DLinkList& L, int i,ElemType e) {
if (i < 1)
return false;
DNode* p;
DNode* s;
int j = 0;
p = L;
//****找到第i-1个结点*****//
while (p != NULL && j < i - 1) { //退出循环后p指向第i-1个结点
p = p->next;
j++;
}
if (p == NULL)
return false;
//*****创建待插入结点s*********//
s = (DNode*)malloc(sizeof(DNode));
s->data = e;
//****在第i-1个结点后进行后插操作****//
InsertNextDNode(p, s);
return true;
}
双链表的整表创建(调用结点后插入)
//手动输入版
//创建双链表
bool CreateListTail(DLinkList& L) {
DNode* p; //p指向当前待插入结点
DNode* r; //r指向尾结点
int e;
cout << "请输入双链表数据,输入999退出" << endl;
cin >> e;
//初始化双链表L
if (InitDLinkList(L) == false)
cout << "初始化双链表头结点失败";
r = L;
while (e != 999) {
//*****创建新的待插入结点*******//
p = (DNode*)malloc(sizeof(DNode));
p->data = e;
//****将新结点p插入到当前尾结点后面*****//
InsertNextDNode(r, p);
//****更新尾指针***********//
r = p;
cin >> e;
}
r->next = NULL;
return true;
}
3. 双链表的删除
删除p结点的后继节点
注意这些边界条件的判断
- 判断p结点是不是为空
- 判断p结点后继是不是最后一个结点
- 判断q结点后继是不是最后一个结点
//删除p结点的后继节点
bool DeleteNextDNode(DNode* p) {
if (p == NULL) return false;
DNode* q = p->next; //找到p结点的后继节点q
if (q == NULL) return false; //p没有后继,本身就是一最后一个结点
p->next = q->next;
if (q->next != NULL) //q结点不是最后一个结点,是最后一个结点,就不用链接这个前驱指针了,直接跳过即可,后面是空的。
q->next->prior = p;
free(q);
return true;
}
整个链表的删除
void DestroyList(DLinkList & L){
//循环释放各个结点
while (L->next != NULL)
DeleteNextDNode(L);
free(L); //释放头结点
L = NULL;//头指针指向NULL
}
4. 双链表的遍历
后向遍历
当p对所有结点做了处理之后(包括最后一个结点),此时,p指向尾结点的后一个结点,此时p == NULL,对这个结点不做处理,退出循环。
while (p != NULL){
//对结点p做相应处理,如打印
p = p->next;
}
前向遍历
当p == NULL时说明当前结点正是头结点的前一个结点,前面一个花括号的内容对头结点做了处理,之后遇到头结点前面一个结点,不做处理,直接退出循环
while (p != NULL){
//对结点p做相应处理
p = p->prior;
}
前向遍历(跳过头结点,不对头结点做处理)
当p->prior == NULL时说明当前结点正是头结点,这是不进入花括号处理,直接退出循环
while (p->prior != NULL){
//对结点p做相应处理
p = p->prior;
}
查找
双链表不可随机存取,按位还是按值查找的操作都只能用遍历的方式实现。时间复杂度O(n).
总结
循环链表
循环单链表
单链表:从一个结点出发只能找到其后续结点
循环单链表的特点:从一个结点出发可以找到其它任何一个结点
//初始化一个循环单链表
bool InitList(LinkList& L) {
L = (Node*)malloc(sizeof(Node));
if (L == NULL)
return false;
//*******头结点的next指针指向头结点******//
L->next = NULL;
return true;
}
//判断循环链表是否为空表: L->next == L
bool Empty(LinkList L) {
if (L->next == L)
return true;
else
return false;
}
//判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L,Node *p) {
if (p->next == L)
return true;
else
return false;
}
循环双链表
1. 初始化
typedef int ElemType;
typedef struct DNode {
ElemType data;
struct DNode* prior, * next;
}DNode,*DLinkList;
//初始化为空的循环双链表
bool InitDLinkList(DLinkList& L) {
L = (DNode*)malloc(sizeof(DNode));
if (L == NULL)
return false;
L->prior = L; //头结点的 prior 指向头结点
L->next = L; //头结点的 next 指向头结点
return true;
}
void test() {
DLinkList L;
InitDLinkList(L); //创建一个空的循环双链表
}
// 判断双链表是否为空
bool Empty(DLinkList L){
if (L->next == L)
retutn true;
else
return false;
}
2. 插入
在p结点之后插入s结点
不必担心p是最后一个结点,不需要做特殊处理*
bool InsertList(DNode *p,DNode *s){
s->next = p->next;
//**********在循环双链表中不必担心p是最后一个结点,不需要做特殊处理***************//
p->next->prior = s;
s->prior = p;
p->next = s;
}
3. 删除
删除p的后继节点q
同样的,不必再担心最后一个结点的问题,实现起来要简单许多
bool DeleteNextDNode(DNode* p) {
if (p == NULL) return false;
DNode* q = p->next;
//*******不必再担心最后一个结点的问题,实现起来要简单许多********//
p->next = q->next;
q->next->prior = p;
free(q);
return true;
}
总结
静态链表
单链表:各个结点在内存中星罗棋布,散落天涯
静态链表:分配一整片连续内存,各个结点集中安置
不用指针,使用数组下标来确定下一个结点的位置
优点:增删操作不需要移动大量元素
缺点:不能随机存取,只能从头结点开始一次往后查找:容量固定不变
每个数组元素为4Byte,每个游标4B(每个结点共8B),设起始地址为e0 = addr
e1 = 存放地址为e1 = addr + 8*游标
用代码定义一个静态链表
//实现方式一:定义结点数组的方式
#define MaxSize 10 //静态链表的最大长度
typedef int ElemType;
struct Node{
ElemType data;
int next; //下一个元素的数组下标
}
void testSLinkList(){
struct Node a[MaxSize]; //定义数组a作为静态链表
//...
}
//实现方式二:结构体数组方式定义
#define MaxSize 10 //静态链表的最大长度
typedef int ElemType;
struct {
ElemType data;
int next; //下一个元素的数组下标
}SLinkList[MaxSize];
void testSLinkList(){
SLinkList a //定义数组a作为静态链表
//...
}
基本操作的实现
查找: 从头结点出发挨个往后遍历结点,O(n)
插入位序为i的结点:
1.找到一个空的结点,存入数据元素
2. 从头结点出发找到位序为i-1的代码
3. 修改新结点的next
4. 修改i-1的结点的next
删除某个结点: 略
顺序表和链表的比较
逻辑结构
都属于线性表,都是线性结构
存储结构
顺序表:
优点:支持随机存取,存储密度高
缺点:大片连续空间分配不方便,改变容量不方便
链表:
优点:离散的小空间分配方便,改变容量方便
缺点:不可随机存取,存储密度低
基本操作
创消、增删改查