CSDN话题挑战赛第2期
参赛话题:学习笔记
前言
本文主要为了掌握单链表基础操作所作的总结,习题部分含408真题或高校真题,以便考研复习。包括但不限于尾插法和前插法建立单链表、“就地逆置”、链式有序表的合并等等。(后期也会继续补充题目。)
注意:
(1)除非特别说明,本文的单链表操作默认是带头结点的。
(2)区分带头结点和不带头结点的单链表的博文,我写了一半,写完就放链接。(如果很久还没放链接,可能博主有事耽搁了,麻烦提醒一下)
(3)可搭配以下链接一起学习:
【考研】线性表的应用之有序表的合并_住在阳光的心里的博客-CSDN博客
【考研】《数据结构》知识点总结.pdf_其它文档类资源-CSDN文库
目录
7、删除递增单链表中重复的元素 Del_Duplicate1()
8、删除单链表中值为 x 的元素的所有结点 Del_Duplicate2()
13、将一个数据域为整数的单链表分割为两个分别为奇数和偶数的链表 Split()
14、查找链表中倒数第 k 个结点 Find_k_Last()
一、 基本操作
1、单链表的结构体定义
// 单链表的存储结构
typedef struct LNode{
ElemType data; //结点的数据域
struct LNode *next; //结点的指针域
}LNode, *LinkList; // LinkList 为指向结构体 LNode 的指针类型
2、初始化单链表 InitLinkList()
//初始化单链表
void InitLinkList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
L->next = NULL:
}
3、尾插法建立单链表 ListInsert_R()
//尾插法
void ListInsert_R(LinkList &L, int a[], int n){
InitLinkList(L); //初始化单链表
/*
L = (LNode *)malloc(sizeof(LNode));
L->next = NULL;
*/
LNode *s, *rear = L; // s 用来指向新申请的结点,rear 指向单链表 L 的终端结点
for(int i = 0; i < n; i++){
s = (LNode *)malloc(sizeof(LNode));
s->data = a[i]; //要插入的结点值
rear->next = s;
rear = rear->next; //更新 rear,指向 L 的终端结点
}
rear->next = NULL;
}
4、 头插法建立单链表 ListInsert_H()
//头插法
void ListInsert_H(LinkList &L, int a[], int n){
LNode *s; // s 用来指向新申请的结点
InitLinkList(L); //初始化单链表
for(int i = 0; i < n; i++){
s = (LNode *)malloc(sizeof(LNode));
s->data = a[i];
s->next = L->next;
L->next = s;
}
}
5、单链表的“就地逆置” ReverseList()
//用头插法实现
void ReverseList(LinkList L){
LNode *p = L->next, *q; // q 起到暂存结点作用
L->next = NULL; //只留下头结点
while(p != NULL){
q = p->next;
p->next = L->next; //头插法
L->next = p;
p = q; //得到下一个结点
}
}
6、逆序打印单链表中的结点 PrintList()
void PrintList(LinkList L){
if(L != NULL){
PrintList(L->next); //递归
printf("%d ", L->data);
}
}
7、删除递增单链表中重复的元素 Del_Duplicate1()
void Del_Duplicate1(LinkList &L){
LNode *p, *q; //q 用来释放被删除的元素
p = L->next;
while(p->next != NULL){
if(p->data == p->next->data){
q = p->next;
p->next = q->next;
free(q);
}
else{
p = p->next;
}
}
}
8、删除单链表中值为 x 的元素的所有结点 Del_Duplicate2()
void Del_Duplicate2(LinkList &L, int x) {
LinkList p, q = L;
p = list->next; //先从链表的第 2 个结点开始判断值是否为 x
while (p != NULL) {
if (p->data == x){
q->next = p->next;
free(p); //释放 p 的空间
p = q->next;
}
else{
q = p;
p = p->next;
}
}
if (list->data == x){ //判断第一个结点值是否为 x
q = L;
L= L->next;
free(q);
}
}
9、查找值为 x 的元素并删除它 FindDel()
int FindDel(LinkList &L, int x){
//找元素 x
LNode *p;
p = L;
while(p->next != NULL){
if(p->next->data == x){
break;
}
p = p->next;
}
//删除它
if(p->next->data == NULL){ //没找到
return 0;
}
else{
LNode *del; //暂存要被删的结点x
del = p->next;
p->next = del->next;
free(del);
return 1;
}
}
10、删除单链表中的最小值 Del_Min()
void Del_Min(LinkList &L){
LNode *p, *pre, *minp, *premin;
p = L->next; // p 用来扫描链表
pre = L; // pre 指向 p 的前驱
minp = p; // minp 指向最小值结点
premin = pre; // premin 指向 minp 的前驱
while(p){
if(p->data < minp->data){
minp = p;
premin = pre;
}
pre = p;
p = p->next;
}
premin->next = minp->next;
free(minp);
}
11、按序号查找结点值 GetElem()
//从单链表中的第一个结点出发,顺指针 next 域逐个往下搜索,
//直到找到第 i 个结点为止,否则返回最后一个结点指针域 NULL
void *GetElem(LinkList L, int i){
int j = 1; //计数
LNode *p = L->next; //头结点指针赋给 p
if(i == 0) return L; //返回头结点
if(i < 1) return NULL; // i 无效,返回 NULL
while(p || j < i){ //从第 i 个结点开始找,查找第 i 个结点
p = p->next;
j++;
}
return p;
}
12、链式有序表的合并 MergeList_L()
//已知单链表 LA 和 LB 的元素按值非递减排列
//归并 LA 和 LB 得到新的单链表 LC, LC 的元素也按值非递减排列
void MergeList_L(LinkList &LA, LinkList &LB, LinkList &LC){
//pa 和 pb 的初值分别指向两个表的第一个结点
pa = LA->next;
pb = LB->next;
LC = LA; //用 LA 的头结点作为 LC 的头结点
pc = LC; //pc 的初值指向 LC 的头结点
while (pa && pb){ //LA和LB均未到达表尾,依次“摘取”两表中值较小的结点插入到 LC 的最后
if (pa->data <= pb->data){ //“摘取” pa 所指结点
pc->next = pa; //将 pa 所指结点链接到 pc 所指结点之后
pc = pa; //pc 指向 pa
pa = pa->next; //pa 指向下一结点
}
else{ //“摘取” pb 所指结点
pc->next = pb; //将 pb 所指结点链接到 pc 所指结点之后
pc = pb; //pc 指向 pb
pb = pb->next; //pb 指向下一结点
}
}
pc->next = pa?pa:pb; //将非空表的剩余段插入到 pc 所指结点之后
delete LB; //释放 LB 的头结点
}
13、将一个数据域为整数的单链表分割为两个分别为奇数和偶数的链表 Split()
void Split(LinkList A, LinkList &B){
LNode *p, *q, *r;
B = (LNode *)malloc(sizeof(LNode));
B->next = NULL;
p = A;
r = B;
while(p){
if(p->next->data % 2 == 0){ // p->next 为偶数
q = p->next;
p->next = q->next; //尾插法
r->next = q;
r = q; //更新 r,让 r 指向新的终端结点
q->next = NULL;
}
else{
p = p->next;
}
}
}
14、查找链表中倒数第 k 个结点 Find_k_Last()
int Find_k_Last(LinkList L, int k){
LNode *p, *q;
p = L->next;
q = L;
int i = 1;
while(p != NULL){
p = p->next;
++i;
if(i > k){
q = q->next;
}
}
if(q == L) return 0; //链表的结点数小于 k
else{
printf("%d ", p->data);
return 1;
}
}
二、习题
1、有两个带头节点的降序单链表 L1、L2,较长的链表元素个数为 n。链表结点定义如下所示。请设计一个时间上尽可能高效算法,按升序输出两个链表中所有元素的权值。
解:(1)算法思想:
将两链表归并到链表 L3 中,归并时采用头插法(输入顺序与线性表中的逻辑顺序是相反的),得到的降序链表 L3,依次输出链表中的元素。
(2)代码如下:
typedef struct LinkNode {
double data; //权值
struct LinkNode *next; //指向单链表的下一个结点
} LinkNode, *Linklist;
LinkNode L3; //定义全局变量 L3
Linklist Head_Insert(Linklist p){ //头插法插入 L3 中
Linklist p1 = p->next;
p->next = L3->next;
L3->next = p;
p = p1;
return p;
}
void ans(Linklist L1, L2){
int t = 0; //t 是新数组中元素个数
Linklist p = L1->next; //p 指向 L1 链表的第一个元素
Linklist q = L2->next; //q 指向 L2 链表的第一个元素
L3->next = null; //初始链表 L3 为空
while (p != null && q != null){ //归并两个降序链表,每次选较大元素
if (p->data > q->data)
p = Head_Insert(p); //将 p 头插法插入 L3 中,p 指向下一个结点
else
q = Head_Insert(q); //将 q 头插法插入 L3 中,q 指向下一个结点
}
while (p != null){ //如果 L1 链表中有剩余元素则头插法插入 L3
p = Head_Insert(p); //将 p 头插法插入 L3 中,p 指向下一个结点
while (q != null) //如果 L2 链表中有剩余元素则头插法插入 L3
q = Head_Insert(q); //将 q 头插法插入 L3 中,q 指向下一个结点
p = L3->next;
while (p != null){ //依次输出链表 L3 中的元素
cout << p->data;
p=p->next;
}
}
2、将两个递增的有序链表合并为一个递增的有序链表。要求结果链表仍使用原来两个链表的存储空间, 不另外占用其它的存储空间。表中不允许有重复的数据。
解:(1)算法思想:
合并后的新表使用头指针 Lc 指向,pa 和 pb 分别是链表 La 和 Lb 的工作指针,初始化为相应链表的第一个结点,从第一个结点开始进行比较。
当两个链表 La 和 Lb 均为到达表尾结点时,依次摘取其中较小者重新链接在 Lc 表的最后。
如果两个表中的元素相等,只摘取 La 表中的元素,删除 Lb 表中的元素,这样确保合并后表中无重复的元素。
当一个表到达表尾结点,为空时,将非空表的剩余元素直接链接在 Lc 表的最后。
(2)代码如下:
//合并链表 La 和 Lb,合并后的新表使用头指针 Lc 指向
void MergeList(LinkList &La, LinkList &Lb, LinkList &Lc){
// pa 和 pb 分别是链表 La 和 Lb 的工作指针,初始化为相应链表的第一个结点
pa = La->next;
pb = Lb->next;
Lc = pc = La; //用 La 的头结点作为 Lc 的头结点
while(pa && pb){ //取较小者 La 中的元素,将 pa 链接在 pc 的后面,pa 指针后移
if(pa->data < pb->data){
pc->next = pa;
pc = pa;
pa = pa->next;
}
else if(pa->data > pb->data) { //取较小者Lb中的元素,将pb链接在pc的后面,pb指针后移
pc->next = pb;
pc = pb;
pb = pb->next;
}
else{ //相等时取La中的元素,删除Lb中的元素
pc->next = pa;
pc = pa;
pa = pa->next;
q = pb->next;
delete pb; //确保合并后表中无重复的元素
pb = q;
}
}
pc->next = pa ? pa : pb; //插入剩余段
delete Lb; //释放 Lb 的头结点
}
3、设计一个算法,通过一趟遍历在单链表中确定值最大的结点。
解:(1)算法思想:
假定第一个结点中数据具有最大值,依次与下一个元素比较,若其小于下一个元素,则设其下一个元素为最大值,反复进行比较,直到遍历完该链表。
(2)代码如下:
int Max (LinkList L){
if(L->next == NULL)
return NULL;
LNode *pmax, *p;
pmax = L->next; //假定第一个结点中数据具有最大值
p = L->next->next;
while(p != NULL){ //如果下一个结点存在
if(p->data > pmax->data)
pmax = p; //如果 p 的值大于 pmax 的值,则重新赋值
p = p->next; //遍历链表
}
return pmax->data;
}
4、设计一个算法,删除递增有序链表中值大于 mink 且小于 maxk 的所有元素(mink 和 maxk 是给定的两个参数,其值可以和表中的元素相同,也可以不同 )。
解:(1)算法思想:
分别查找第一个值 > mink 的结点和第一个值 ≥ maxk 的结点,再修改指针,删除值大于 mink 且小于 maxk 的所有元素。
(2)代码如下:
void delete(LinkList &L, int mink, int maxk){
p = L->next; //首元结点
while (p && p->data <= mink){ //查找第一个值 > mink的结点
pre = p;
p = p->next;
}
if (p){
while (p && p->data < maxk) // 查找第一个值 ≥ maxk 的结点
p = p->next;
q = pre->next;
pre->next = p; // 修改指针
while (q != p){
s = q->next;
delete q; // 释放结点空间
q = s;
}
}
}
5、设线性表L1 = (a1, a2, a3, ...., an-2, an-1, an ) 采用带头结点的单链表保存,链表中的结点定义如下:
typedef struct node{ int data; struct node*next; }NODE;
请设计一个空间复杂度为 O(1) 且时间上尽可能高效的算法,重新排列 L1 中的各结点,得到线性表 L2 = (a1, an, a2, an-1, a3, an-2, ....),要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用 C 或 C++语言描述算法,关键之处给出注释。
(3)说明你所设计的算法的时间复杂度。
解:(1)算法思想:
先观察 L1 = (a1, a2, a3, ...., an-2, an-1, an ) 和L2 = (a1, an, a2, an-1, a3, an-2, ....)
发现 L2 是由 L1摘 取第一个元素,再摘取倒数第一个元素……依次合并而成的。
为了方便链表后半段取元素,需要先将 L1 后半段原地逆置[题目要求空间复杂度为 O(1),不能借助栈],否则每取最后一个结点都需要遍历一次链表。
① 先找出链表 L1 的中间结点,为此设置两个指针 p 和 q,指针 p 每次 走一步,指针 q 每次走两步,当指针 q 到达链尾时,指针 p 正好在链表的中间结点;
② 然后将 L1 的后半段结点原地逆置。
③ 从单链表前后两段中依次各取一个结点,按要求重排。
(2)代码如下:
void change_list(NODE *h)
{
NODE *p, *q, *r, *s;
p = q = h;
while (q->next != NULL) //寻找中间结点
{
p = p->next; //p 走一步
q = q->next;
if (q->next != NULL)
q = q->next; //q 走两步
}
q = p->next; //p 所指结点为中间结点,q 为后半段链表的首结点
p->next = NULL;
while (q != NULL) //将链表后半段逆置
{
r = q->next;
q->next = p->next;
p->next = q;
q = r;
}
s = h->next; //s 指向前半段的第一个数据结点,即插入点
q = p->next; //q 指向后半段的第一个数据结点
p->next = NULL;
while (q != NULL) //将链表后半段的结点插入到指定位置
{
r = q->next; //r 指向后半段的下一个结点
q->next = s->next; //将 q 所指结点插入到 s 所指结点之后
s->next = q;
s = q->next; //s 指向前半段的下一个插入点
q = r;
}
}
答案来源链接:牛客网 (nowcoder.com)