为了学习数据结构我决定在自己跟着书打一遍,跟的是王道考研教材,命名没有严格按照严蔚敏老师的来。所以注意不要学我写的命名。为了加深我对这个板块儿学习的理解,我打算在这里总结一遍,反思教材的同时加入自己的理解。
链式结构和顺序结构
链式结构是一种物理结构,也就是数据在计算机中存储的方式,顺序表采用的是顺序结构。二者不同之处在于顺序结构中每个元素都是紧挨在一起的,书上的说法是“它是用一组连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻”。我们通常用数组来申请一片连续的空间,因此顺序表总与数组紧密联系。顺序表的好处是存储密度大,也就是整个线性表中存储的信息基本上都是对我们有用处的信息。而链表则不是,这是因为链表中除了存储对我们有价值的信息外还存了一些地址信息,这些地址是为了让计算机能够找到下一个节点,对我们是没有什么用的,因此链表的存储密度小。
顺序表的另一好处是随机存取,因为我们有数组首地址,同时还知道这一片区域中的每个数据元素都是一个接一个挨着的,所以我们根据下标就能直接找到对应下标的值。有个老师曾经问过我们一个问题,从长度为一亿的数组中取第435678个数(具体是几个我也忘了,反正就是这个意思),时间复杂度是多少?我们都陷入沉思,然后他邪魅一笑,说:O(1)啊。可见顺序表的存取效率是很高的。
链表的存取效率就很低,因为它只能进行顺序存取,我们通常只能知道链表中的一个或几个节点的位置,其余的位置都是未知的,我们必须通过一个节点的next指针找到下一个,所以我们在找一个想要的元素时必须一个接一个的找,不能像顺序表那样直接通过索引取出一个来,因此链表的存取效率比顺序表低。综上所述:当一个线性表需要经常查询和修改时,我们用顺序表实现效率更高。
然而当涉及删除和插入操作时,顺序表的效率就低下来了。因为顺序表要求数据元素必须一个接着一个的存,在物理结构上也要相邻,所以当我们要删除一个位于数组中间的元素,它会留下一个空缺,它后边的元素就都要前移来补这个空缺。而我们要在顺序表中间插入一个元素时,我们就要给他腾出一个空缺让它进去,为了腾出空缺我们必须要让这个特定位置之后的所有元素都向后移动。这非常花时间。
而链表就不用这个操作,因为链表是靠地址链接起来的,所以当我们想往链表中间位置插入一个元素时,我们只需要找到插入位置的前驱节点,然后改next指针即可。单纯的从插入这个操作来将它的时间复杂度是O(1)。所以当一个顺序表需要频繁进行插入和删除操作时,我们最好用链表来做。
链式结构实现线性表
1.准备工作
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
LinkList Init_Linklist(void){//初始化一个带头节点,这个头节点会被认为代表整个单链表
LinkList L = (LNode*)malloc(sizeof(LNode));
L->data = 0;
L->next = NULL;
return L;
}
定义了结构体,我还专门写了一个函数来创建和初始化一个新的链表。
2.头插法建立单链表
void List_HeadInsert(LinkList L){
LNode *s;int i;//定义临时节点变量s和存储它的数据的变量i
printf("输入想要插入的数据:\n");
scanf("%d",&i);
while (i!=9999) {
s = (LNode*)malloc(sizeof(LNode));//给s一片空间
s->data = i;//给I赋值
s->next = L->next;//首先将头节点的下一个元素地址给s。
L->next = s;//然后将L的下一个元素地址更新为s,这样就把他们链接到了一起
scanf("%d",&i);
L->data++;//我在头节点的数据域存了长度,长度要自增1
}
return;
}
在操作中,我们必须先将s的next指针更新头指针的next指针,否则头指针的next指针会丢失,操作就完成不了了。
3.尾插法建立单链表
void List_TailInsert(LinkList L){
int i;
LNode *tail = L,*s;//尾插法需要定义一个尾指针
printf("输入想要插入的数据:\n");
scanf("%d",&i);
while (i!=9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = i;
tail->next = s;//首先将s插入到尾指针之后
s->next = NULL;//更新s的next指针为空,这个操作什么时候都可以做
tail = s;//尾指针更新为s,s成为了新的尾指针
scanf("%d",&i);
L->data++;//长度自增
}
return;
}
尾插法需要指明一个尾指针,我们要不断的向它的尾部插入数据,这个方法其实比较容易理解,就是不断的将尾节点的next更新为新节点的地址,同时每一次更新都伴随着新节点更新为新的尾节点。这样我们重复这个过程就可以建立链表。
值得注意的是我们通过头插法建立的链表是逆序的,而通过尾插法建立的链表是顺序的。我们可以通过头插法来逆置链表,这是一个很有用的方法。
4.按位序查找
LNode *GetElem(LinkList L,int i){
if(i>L->data){
printf("当前链表并没有那么长。\n");
return L;
}//错误数据排除
if(i<1){
printf("当前链表并没有这么短。\n");
return L;
}//错误数据排除
LNode *p = L;int j = 0;//一开始p指向头节点,j代表p指向节点的位序,我们认为头节点是第0个节点
for(;j<i;j++){
p = p->next;
}//循环并找到
printf("查找到第%d个节点的值为%d。\n",i,p->data);
return p;
}
这个功能容易理解。
5.按值查找
LNode *LocateElem(LinkList L,int e){
int i = 1;
LNode *p = L->next;//设置p初始位置
while(p!=NULL){
if(p->data==e)
break;
p = p->next;
i++;
}//顺序遍历,知道找到目标值为止
printf("检索到目标节点,其位置位于第%d个位置。\n",i);
return p;
}
这个功能也不难理解。
6.插入数据
void Insert(LinkList L, int position){
int i = 0,x;
LNode *p = L,*s = (LNode*)malloc(sizeof(LNode));//s是临时节点
if(position<1||position>L->data+1){
printf("非法位置。");
}//对于非法位置的排除
for(;i<position-1;i++){
p = p->next;
}//我们找到目标位置的前一个节点
scanf("%d",&x);
s->data = x;
s->next = p->next;
p->next = s;
L->data++;
return;//将s插入到目标位置的前一个节点。这样就相当于将其加到了目标位置
}
我们从头节点开始,查询目标位置的前一个位置,插入到它的后边即可。
7.按位删除
void DeleteElem(LinkList L,int i){
LNode *p = GetElem(L, i-1);//使用之前写好的代码很方便
LNode *q = p->next;
p->next = q->next;
free(q);
}
注意这里首先调用之前的按位查找函数,找到目标位置的前驱节点,然后让它的next节点指向将要被删除的节点的next节点,这样就把目标删除节点脱离出来了。然后再free这个节点即可。
链表的操作基本上就是这些,要想学习好数据结构做题是不够的,一定要动手打代码,多学多练,熟能生巧。理解自然也会加深。我就要多练,最后我把我的整个库文件放到这里,里边有两个扩展功能,有兴趣者可以自行学习研究,书里边也有。我基本上是照着打的。
//
// linkoption.h
// 线性表的链式表示
//
// 2020/10/16.
//
// 此库文件将提供关于单链表的各种方法
#ifndef linkoption_h
#define linkoption_h
#include <stdio.h>
#include <stdlib.h>
typedef struct LNode{
int data;
struct LNode *next;
}LNode, *LinkList;
LinkList Init_Linklist(void){//初始化一个带头节点,这个头节点会被认为代表整个单链表
LinkList L = (LNode*)malloc(sizeof(LNode));
L->data = 0;
L->next = NULL;
return L;
}
//头插法建立起链表
void List_HeadInsert(LinkList L){
LNode *s;int i;
printf("输入想要插入的数据:\n");
scanf("%d",&i);
while (i!=9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = i;
s->next = L->next;
L->next = s;
scanf("%d",&i);
L->data++;
}
return;
}
//尾插法建立起链表
void List_TailInsert(LinkList L){
int i;
LNode *tail = L,*s;
printf("输入想要插入的数据:\n");
scanf("%d",&i);
while (i!=9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = i;
tail->next = s;
s->next = NULL;
tail = s;
scanf("%d",&i);
L->data++;
}
return;
}
//按序号查找
LNode *GetElem(LinkList L,int i){
if(i>L->data){
printf("当前链表并没有那么长。\n");
return L;
}
if(i<1){
printf("当前链表并没有这么短。\n");
return L;
}
LNode *p = L;int j = 0;
for(;j<i;j++){
p = p->next;
}
printf("查找到第%d个节点的值为%d。\n",i,p->data);
return p;
}
//按值查找
LNode *LocateElem(LinkList L,int e){
int i = 1;
LNode *p = L->next;
while(p!=NULL){
if(p->data==e)
break;
p = p->next;
i++;
}
printf("检索到目标节点,其位置位于第%d个位置。\n",i);
return p;
}
void show_the_list(LinkList L){
LNode *p=L->next;
printf("当前链表长度为:%d\n",L->data);
while(p!=NULL){
printf("%d ",p->data);
p = p->next;
}
printf("\n");
return;
}
//插入节点操作
void Insert(LinkList L, int position){
int i = 0,x;
LNode *p = L,*s = (LNode*)malloc(sizeof(LNode));
if(position<1||position>L->data+1){
printf("非法位置。");
}
for(;i<position-1;i++){
p = p->next;
}
scanf("%d",&x);
s->data = x;
s->next = p->next;
p->next = s;
L->data++;
return;//这个算法在挑选位置上下了些功夫,数学计算上用了心思,所以我们只用一种方法就能够保证其前后边界的稳定性
//这就是中庸思想。
}
//对某一节点进行前插和后插操作
void Insert_by_LNode(LNode *aim_LNode,int flag,LinkList L){
LNode *s = (LNode*)malloc(sizeof(LNode));
int t;
if(flag == 0){//我们认为“0”代表后插
printf("请输入想插入的数字:");
scanf("%d",&s->data);
s->next = aim_LNode->next;
aim_LNode->next = s;
L->data++;
return;
}else if(flag == 1){//我们认为1代表前插
if(aim_LNode==L){
printf("此节点不支持前插操作。");
return;
}
printf("请输入想插入的数字:");
scanf("%d",&s->data);
s->next = aim_LNode->next;
aim_LNode->next = s;
t = s->data;
s->data = aim_LNode->data;
aim_LNode->data = t;//交换操作
L->data++;
return;
}else{
printf("你输入了错误的指令。\n");
printf("操作将退出。\n");
return;
}
}
void DeleteElem(LinkList L,int i){
LNode *p = GetElem(L, i-1);//使用之前写好的代码很方便
LNode *q = p->next;
p->next = q->next;
free(q);
}
void DeleteNode(LNode *p,LinkList L){
int t;
LNode *q;
if(p->next==NULL){
q = L;
while(q->next!=p){
q = q->next;
}
q->next=NULL;
free(p);
return;
}
q = p->next;
t = p->data;
p->data = q->data;
q->data = t;//互换灵魂
p->next = q->next;
free(q);
return;
}
#endif /* linkoption_h */