
本文共 9294 字,大约阅读时间需要 30 分钟。
第3章 顺序表的链式存储
数据结构与算法_师大完整教程目录(更有python、go、pytorch、tensorflow、爬虫、人工智能教学等着你):
一、链式存储
- 解决问题:对于线性结构,使用顺序存储,需要足够大的连续存储区域
- 链式存储:结点除了存放信息,并且附设指针,用指针体现结点之间的逻辑关系
- 注:\(c\)语言的动态分配函数\(malloc()\)和\(free()\)分别实现内存空间的动态分配和回收,所以不必知道某个结点的具体地址
- 注:链式存储中,必须有一个指针指向第一个结点的存储位置,一般为\(head\)标示
- 顺序存储和链式存储的区别:顺序存储更适合查询量大的程序设计;链式存储更适合需要频繁插入和删除的程序
二、单链表
2.1 单链表的基本概念及描述
- 单链表结点构造:两个域,一个存放数据信息的\(info\)域;另一个指向该结点的后继结点的\(next\)域
2.2 单链表的实现
单链表的常用操作:
- 建立一个空的单链表
- 输出单链表中各个结点的值
- 在单链表中查找第\(i\)个结点
2.2.1 单链表的存储结构
typedef int datatype;typedef struct link_node { datatype info; struct link_node *next;} node;
2.2.2 单链表的插入操作(算法)
算法步骤(插入结点为\(p\),插入到结点\(q\)后面):
- 通过
find(head,i)
查找\(q\)结点,查不到打印报错信息 - 给插入结点\(p\)分配空间,并设置信息
- 如果在单链表的最前面插入新结点,让单链表的首指针指向新插入的结点
- 通过
p->next = head;
head = p;
6. 如果在单链表中间插入新结点: p->next = q->next;
q->next=p;
typedef int datatype;typedef struct link_node { datatype info; struct link_node *next;} node;node *insert(node *head, datatype x, int i) { node *p, *q; q = find(head, i); // 查找第i个结点 if (!q && i != 0) { printf("\n找不到第%d个结点,不能插入%d!", i, x); } else { p = (node *) malloc(sizeof(node)); // 分配空间 p->info = x; // 设置新结点 if (i == 0) // 插入的结点作为单链表的第一个结点 { p->next = head; head = p; } else { p->next = q->next; // 后插 q->next = p; } } return head;}
2.2.3 单链表的删除操作(算法)
算法步骤(被删除结点\(q\),被删除结点前一个结点\(pre\))
- 判断链表是否为空
- 循环查找被删除结点\(q\),并且设置一个结点\(pre\)标示被删除结点的前一个结点
- 如果删除结点为第一个结点
head = head->next;
free(p)
6. 如果删除结点为其他结点 pre->next = q->next;
free(p)
typedef int datatype;typedef struct link_node { datatype info; struct link_node *next;} node;node *dele(node *head, datatype x) { node *pre = NULL, *p; if (!head) { printf("单链表是空的"); return head; } p = head; while (p && p->info != x) // 寻找被删除结点p { pre = p; // pre指向p的前驱结点 p = p->next; } if (p) { if (!pre) // 被删除结点没有上一个结点,则是要删除的是第一个结点 { head = head->next; } else { pre->next = p->next; } free(p) } return head;}
三、带头结点的单链表
3.1 带头结点的单链表的基本概念及描述
- 头结点的作用:单链表的插入和删除需要对空的单链表进行特殊处理,因此可以设置 \(head\) 指针指向一个永远不会被删除的结点——头结点
- 注:\(head\) 指示的是所谓的头结点,它不是实际结点,第一个实际结点应该是
head->next
指示的
3.2 带头结点的单链表的实现
带头结点的单链表的常用操作:
- 建立一个空的带头结点的单链表
- 输出带头结点的单链表中各个结点的值
- 在带头结点的单链表中查找第 \(i\) 个结点
3.2.1 带头结点的单链表的存储结构
typedef int datatype;typedef struct link_node { datatype info; struct link_node *next;} node;
3.2.2 带头结点的单链表的插入(算法)
算法步骤( \(p\) 为插入结点,\(q\) 为插入前一个结点):
- 通过
find(head,i)
查找带头结点的单链表中的第 \(i\) 个结点( \(i=0\) 表示新结点插入在头结点之后) - 如果没找到结点 \(q\),打印报错信息
- 如果在非空的带头结点的单链表最前面插入一个新结点
- 通过
p->next = q->next;
q->next = p;
6. 如果在非空的带头结点的单链表的内部插入一个新结点 p->next = q->next;
q->next = p;
typedef int datatype;typedef struct link_node { datatype info; struct link_node *next;} node;node *insert(node *head, datatype x, int i) { node *p, *q; q = find(head, i); // 查找带头结点的单链表中的第 i 个结点,i=0 时表示新结点插入在头结点之后 if (!q) // 没有找到 { printf("\n带头结点的单链表中不存在第%d个结点!不能插入%d!", i, x); return head; } p = (node *) malloc(sizeof(node)); // 为准备插入的新结点分配空间 p->info = x; // 为新结点设置值 p->next = q->next; q->next = q; // i=0 时,本语句等价于 head->next=p return head;}
3.2.3 带头结点的单链表的删除(算法)
算法步骤(被删除结点为 \(q\),被删除结点的前一个结点为 \(pre\)):
- 设置 \(pre\) 指向头结点
- \(q\) 从带头结点的单链表的第一个实际结点开始循环寻找值为 \(x\) 的结点
- 删除带头结点的单链表的第一个实际结点:
pre->next = q->next;
free(q)
6. 删除带头结点的单链表的内部结点: pre->next = q->next;
free(q)
typedef int datatype;typedef struct link_node { datatype info; struct link_node *next;} node;node *dele(node *head, datatype x) { node *pre = head, *q; // pre 指向头结点 q = head->next; // q 从带头结点的单链表的第一个实际结点开始找值为 x 的结点 while (q && q->info != x) // 循环查找值为 x 的结点 { pre = q; // pre 指向 q 的前驱 q = q->next; } if (q) { pre->next = q->next; // 删除 free(q); // 释放内存空间 } return head;}
四、循环单链表
4.1 循环单链表的基本概念及描述
- 单链表存在的问题:从表中的某个结点开始,只能访问该结点后面的结点
- 循环单链表解决的问题:从表中的任意一个结点开始,使其都能访问到表中的所有的结点
- 循环单链表:在单链表的基础上,设置表中最后一个结点的指针域指向表中的第一个结点
4.2 循环单链表的实现
循环单链表的常用操作:
- 建立一个空的循环单链表
- 获得循环单链表的最后一个结点的存储地址
- 输出循环单链表中各个结点的值
- 在循环单链表中查找一个值为 \(x\) 的结点
- 循环单链表的插入操作
- 循环单链表的删除操作
- 循环单链表的整体插入与删除操作
4.2.1 循环单链表的存储结构
typedef int datatype;typedef struct link_node { datatype info; struct link_node *next;} node;
五、双链表
5.1 双链表的基本概念及描述
- 双链表解决的问题:设置一个 \(llink\) 指针域,通过这个指针域直接找到每一个结点的前驱结点
5.2 双链表的实现
双链表的常用操作:
- 建立一个空的双链表
- 输出双链表中各个结点的值
- 查找双链表中第 \(i\) 个结点
- 双链表的插入操作
- 双链表的删除操作
5.2.1 双链表的存储结构
typedef int datatype;typedef struct dlink_node { datatype info; struct dlink_node *llink, *rlink;} dnode;
六、链式栈
6.1 链式栈的基本概念及描述
- 链式栈:使用链式存储的栈
- 注:链式栈的栈顶指针一般用 \(top\) 表示
6.2 链式栈的实现
链式栈的常用操作:
- 建立一个空的链式栈
- 判断链式栈是否为空
- 取得链式栈的栈顶结点值
- 输出链式栈中各个结点的值
- 向链式栈中插入一个值为 \(x\) 的结点
- 删除链式栈的栈顶节点
6.2.1 链式栈的存储结构
typedef int datatype;typedef struct link_node { datatype info; struct link_node *next;} node;
七、链式队列
7.1 链式队列的基本概念及描述
- 链式队列:使用链式存储的队列
- 注:队列必须有队首和队尾指针,因此增加一个结构类型,其中的两个指针域分别为队首和队尾指针
7.2 链式队列的实现
链式队列的常用操作:
- 建立一个空的链式队列
- 判断链式队列是否为空
- 输出链式队列中各个结点的值
- 取得链式队列的队首结点值
- 向链式队列中插入一个值为 \(x\) 的结点
- 删除链式队列中的队首结点
7.2.1 链式队列的存储结构
typedef int datatype;typedef struct link_node { datatype info; struct link_node *next;} node;typedef struct { node *front, *rear; // 定义队首和队尾指针} queue;
八、算法设计题
8.1 求单链表中结点个数(算法)
设计一个算法,求一个单链表中的结点个数
typedef struct node { int data; struct node *next;} linknode;typedef linknode *linklist;int count(linklist head) { int c = 0; linklist p = head; // head为实际的第一个结点 while (p) // 计数 { c++; p = p->next; } return c;}
8.2 求带头结点的单链表中的结点个数(算法)
设计一个算法,求一个带头结点单链表中的结点个数
typedef struct node { int data; struct node *next;} linknode;typedef linknode *linklist;int count(linlist head) { int c = 0; linklist = head->next; // head->next 为实际的第一个结点 while (p) // 计数 { c++; p = p->next; } return c;}
8.3 在单链表中的某个结点前插一个新结点(算法)
设计一个算法,在一个单链表中值为 y 的结点前面插入一个值为 x 的结点。即使值为 x 的新结点成为值为 y 的结点的前驱结点
typedef struct node { int data; struct node *next;} linknode;typedef linknode *linklist;void insert(linklist head, int y, int c) { linklist pre, p, s; // 假设单链表带头结点 pre = head; p = head->next; while (p && p->data != y) { pre = p; p = p->next; } if (p) // 找到了值为 y 的结点,即 p == y { s = (linklist) malloc(sizeof(linknode)); s->data = x; s->next = p; pre->next = s; }}
8.4 判断单链表的各个结点是否有序(算法)
设计一个算法,判断一个单链表中各个结点值是否有序
typedef struct node { int data; struct node *next;} linknode;typedef linknode *linklist;int issorted(linklist head, char c) // c='a' 时为升序,c='d' 时为降序{ int flag = 1; linklist p = head->next; switch (c) { case 'a': // 判断带头结点的单链表 head 是否为升序 while (p && p->next && flag) { if (p->data <= p->next->data) p = p->next; else flag = 0; } break; case 'd': // 判断带头结点的单链表 head 是否为降序 while (p && p->next && flag) { if (p->data >= p->next->data) p = p->next; else flag = 0 } break; } return flag;}
8.5 逆转一个单链表(算法)
设计一个算法,利用单链表原来的结点空间将一个单链表就地转置
核心思想:通过
head->next
保留上一个 \(q\) 的状态算法步骤:
- 让 \(p\) 指向实际的第一个结点
- 循环以下步骤:
让 \(p\) 一直循环下去,直到走完整个链表,\(p\) 循环的时候,\(q\) 跟着 \(p\) 一起刷新
\(q\) 的 \(next\) 指针域始终指向head->next;
head->next;
始终指向上一个 \(q\) typedef struct node { int data; struct node *next;} linknode;typedef linknode *linklist;void verge(linklist head) { linlist p, q; p = head->next; head->next = NULL; while (p) { q = p; p = p->next; q->next = head->next; // 通过 head->next 保留上一个 q 的状态 head->next = q; }}
8.6 拆分结点值为自然数的单链表,原链表保留值为偶数的结点,新链表存放值为奇数的结点(算法)
设计一个算法,将一个结点值自然数的单链表拆分为两个单链表,原表中保留值为偶数的结点,而值为奇数的结点按它们在原表中的相对次序组成一个新的单链表
typedef struct node { int data; struct node *next;} linknode;typedef linknode *linklist;linklist sprit(linklist head) { linklist L, pre, p, r; L = r = (linklist) malloc(sizeof(linknode)); r->next = NULL; pre = head; p = head->next; while (p) { if (p->data % 2 == 1) // 删除奇数值结点,并用 L 链表保存 { pre->next = p->next; r->next = p; r = p; // 这样使得 r 变成了 r->next p = pre->next; // 这样使得 p 变成了 head->next->next } else // 保留偶数值结点 { pre = p; // 书中的貌似多余操作 p = p->next; } } r->next = NULL; // 置返回的奇数链表结束标记 return L;}
8.7 在有序单链表中删除值大于 x 而小于 y 的结点(算法)
设计一个算法,对一个有序的单链表,删除所有值大于 x 而不大于 y 的结点
typedef struct node { int data; struct node *next;} linknode;typedef linknode *linklist;void deletedata(linklist head, datatype x, datatype y) { linklist pre = head, p, q; p = head->next; // 找第 1 处大于 x 的结点位置 while (p && p->data <= x) { pre = p; p = p->next; } // 找第 1 处小于 y 的位置 while (p && p->data <= y) p = p->next; // 删除大于 x 而小于 y 的结点 q = pre->next; pre->next = p; // 小于 x 的第一个结点指向大于 y 的第一个结点 pre = q->next; // 释放被删除结点所占用的空间 while (pre != p) { // 此时 p 已经指向了大于 y 的第一个结点 free(q); q = pre; pre = pre->next; }}
九、错题集
在头结点的单链表中查找 \(x\) 应选择的程序体是:
node *p = head; while (p && p->info != x) p = p->next; return p;
- 注:未找到时需要返回头结点 \(head\),而不是返回一个 \(NULL\)
用不带头结点的单链表存储队列时,其队头指针指向队头结点,其队尾指针指向队尾结点,则在进行删除操作时队头队尾指针都可能要修改
- 注:链式队列中只有一个结点是会出现该情况,插入时同理
若从键盘输入 \(n\) 个元素,则建立一个有序单向链表的时间复杂度为 \(O(n^2)\)
- 注:第 \(1\) 个数:\(0\) 次查找;第 \(2\) 个数:\(1\) 次查找 \(,\cdots,\) 第 \(n\) 个数,\(n-1\) 次查找,总共 \(n(n-1)/2\) 次
发表评论
最新留言
关于作者
