单链表的删除实战
12.3 单链表的删除实战
12.3.1 流程图
- 不存在(除紧跟链表的第一个之外的,其他元素)也算删除成功!!!,但其实我们考虑的都是在链表中元素的删除!!!
但是,如果前一个结点不存在,就可以甩锅,说明删除失败,是前一个点的原因!!!
纠结的点!!!:(完全没必要)
如果删除的是紧跟链表的第一个,会报错,因为此时不会return false(该位置前一个结点p是存在的),并且p->next
值为 NULL
,free(NULL)
是无法进行编译的,所以出错。
以上纯属个人钻牛角尖,其实单链表的删除,肯定是在链表中元素的删除,不会在已有结点之外。!!!
- 为什么这边一定要
free()
函数,因为删除呀,你肯定要释放空间呀,所以要free
!!!
12.3.2 代码
#include <stdlib.h>
#include <stdio.h>
//定义结构体
typedef int ElemType;
typedef struct LNode{
ElemType data;
LNode *next;
}LNode, *LinkList;
//尾插法新建链表
void LinkList_tail_insert(LinkList &L){
L = (LinkList) malloc(sizeof (LNode));
LinkList s, r = L;
int x;
scanf("%d", &x);
while(x != 9999){
s= (LinkList) malloc(sizeof (LNode));
s->data = x;
r->next = s;
r = s;
scanf("%d", &x);
}
r->next = NULL;
}
//打印链表
void print_LinkList(LinkList L){
L = L->next;
while(L){
printf("%3d", L->data);
L = L->next;
}
printf("\n");
}
//找到指向第i个位置的指针
LinkList select_pos(LinkList L, int pos){
int i = 0;//从第0个位置开始,且第0个位置是可以查的
//判断所查位置的合理性
if(pos < 0){
return NULL;
}
while(L && i < pos){
L = L->next;
i++;
}
return L;
}
//删除链表
bool LinkList_Delete(LinkList &L, int pos){
LinkList p = select_pos(L, pos-1);
if(NULL == p){
return false;
}
LinkList q = p->next;
p->next = q->next;
free(q);//重要!!!
return true;
};
int main() {
LinkList L;
LinkList_tail_insert(L);
print_LinkList(L);
bool res;
res = LinkList_Delete(L, 5);
if(res){
printf("success\n");
print_LinkList(L);
}else{
printf("failure\n");
print_LinkList(L);
}
return 0;
}
12.4 2019年41题–题目解读与解题设计
12.4.1 题目简介
12.4.2 题目解读
- 空间复杂度为O(1),即我们不能申请额外的空间(如当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1))。如下图所示
- 解题步骤:
- 找到链表的中间结点
- 前面一半为链表
L
(即原来的头结点),后面一半为链表L2
(新的头结点);- 将链表
L2
进行原地逆置- 将
L
与L2
合并
12.4.3 解题设计
12.4.3.1 如何找到链表的中间结点?
定义俩个指针p_front
和p_back
- p_front 在前面,每次先走俩步
- p_back 在后面,每次再走一步
- 直到p_front值为NULL,p_front不走了,p_back也不走了
(1)当n为奇数时:
(2)当n为偶数时:
==!!!==一般默认第一个链表放元素多的元素,所以此时L:{a1、a2、a3}
;L2:{a4、a5、a6}
;
综上:如上述操作完毕后,头结点保持不动,新建头结点L2
指向p_back的后一个结点即可。
12.4.3.2 如何将L2
逆置?
- 流程图
- 示意图
初始状态
a1->a2->a3->a4->a5->a6->NULL
第一次移动之前:
a1<-a2,a3->a4->a5->a6->NULL
此时
a1<-a2<-a3,a4->a5->a6->NULL
此时
a1<-a2<-a3<-a4,a5->a6->NULL
此时
a1<-a2<-a3<-a4<-a5,a6->NULL
此次由于t已经变成NULL了,此时r指向倒数第二个元素,s指向倒数第一个元素,再进行一次
s->next=r
此时
a1<-a2<-a3<-a4<-a5<-a6
,但由于默认最后一个结点的指针域为NULL,所以L2->next->next=NULL
此时
NULL<-a1<-a2<-a3<-a4<-a5<-a6
,并且重新赋值头指针,将新的第一个元素赋值给头指针L2->s
12.4.3.3 将L和L2
链表合并,合并时轮流放入一个结点
- 流程图
2.动态示意图
首先:
pcur=L->next;p=p->next;q=q->next;
指针 作用 pcur
指示当前链表最后一个元素 p 指向链表L中将要被合并的元素 q 指向链表 L2
中将要被合并的元素此时因为L中第一个元素不动,所以p=p->next
pcur->next=q;
q=q->next;
pcur=pcur->next;
pcur->next=p;
p=p->next;
pcur=pcur->next;
循环代码见上图;
但是此时的p指针指向了NULL,所以下一次并不进入循环,而是直接将q指针所指的元素,接在
pcur
的后面。
12.5 真题题目代码实战
注意点!!!
L2
需要分配头结点;- 在找到中间结点的子函数中,要注意
p_front
指针的合理性,区别于n为奇数和偶数的情况!!!,且空指针的next不存在; - 注意修改新旧链表的头部与尾部;
- 逆置的时候,考虑
L2
没有结点和只有一个结点的特例; - 合并时,p从第二个结点开始,因为第一个结点固定了;
#include <stdio.h>
#include <stdlib.h>
//定义结构体
typedef int ElemType;
typedef struct LNode{
ElemType data;
LNode *next;
}LNode, *LinkList;
//头插法新建链表
void LinkList_head_insert(LinkList &L){
L = (LinkList) malloc(sizeof (LNode));
L->next = NULL;
LinkList s;
int x;
scanf("%d", &x);
while(x != 9999){
s = (LinkList) malloc(sizeof (LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d", &x);
}
}
//打印链表
void print_LinkList(LinkList L){
L = L->next;
while(L){
printf("%3d", L->data);
L = L->next;
}
printf("\n");
}
//找到中间结点,并且使头结点L2指向中间结点以后的链表,并且修改L
void find_middle(LinkList &L, LinkList &L2){
L2 = (LinkList) malloc(sizeof (LNode));//给L2的头结点分配空间
LinkList p_front, p_back;
p_front = L->next;
p_back = L->next;
while(p_front){
p_front = p_front->next;
//NULL是没有next的值的,系统会报错,所以我们提前退出程序(当n为奇数的退出条件)
if(!p_front){
break;
}
p_front = p_front->next;
//为了保证n为偶数个时的均匀分配(当n为偶数的退出条件)
if(!p_front){
break;
}
p_back = p_back->next;
}
L2->next = p_back->next;
p_back->next = NULL;
}
//逆置L2
void reverse_LinkList(LinkList &L2){
LinkList r,s,t;
r = L2->next;
//如果没有结点,直接返回,不需要操作
if(!r){
return;
}
//如果为一个结点,直接返回,不需要操作
s = r->next;
if(!s){
return;
}
t = s->next;
while(t){
s->next = r;
r = s;
s = t;
t = t->next;
}
//最后的倒数第一和倒数第二的元素
s->next = r;
//逆置之后的最后一个元素的指针域赋值为NULL
L2->next->next = NULL;
//逆置之后,要把头结点连接上新的第一个元素
L2->next = s;
}
//将L与新L2合并
void merge_LinkList(LinkList &L, LinkList &L2){
LinkList pcur, p, q;
//pcur指示合并链表的最后一个结点
pcur = L->next;
//q指示L2链表的准备进入的结点
q = L2->next;
//p指示L链表的准备进入的结点
p = L->next;
//因为L中第一个结点不动,所以p应该指向L2的第二个结点
p = p->next;
while(p && q){
//因为第一个元素为L的第一个元素,所以先存q,再存p
pcur->next = q;//pcur指向q
pcur = pcur->next;//pcur向后移
q = q->next;//q向后移
pcur->next = p;//与q同理
pcur = pcur->next;
p = p->next;
}
//L可能还剩下一个,加上去
if(p){
pcur->next = p;
}
//L2可能还剩下一个,加上去
if(q){
pcur->next = q;
}
}
int main() {
LinkList L, L2;
LinkList_head_insert(L);
print_LinkList(L);
printf("--------------------------\n");
find_middle(L, L2);
print_LinkList(L);
print_LinkList(L2);
printf("--------------------------\n");
reverse_LinkList(L2);
print_LinkList(L);
print_LinkList(L2);
printf("--------------------------\n");
merge_LinkList(L,L2);
print_LinkList(L);
return 0;
}
12.6 分析真题代码的时间复杂度
find_middle()
函数,循环的次数为n/2
,所以时间复杂度为O(n);reverse_LinkList()
函数,只遍历了L2
链表,遍历长度为n/2
,所以时间复杂度为O(n);merge_LinkList()
函数,循环遍历次数也为n/2
,所以时间复杂度为O(n);