22.线性表链式存储结构--单链表
一.单链表知识引入
由浅入深写代码(分解问题),通过画图来理清思路。
各种结构没有好坏之分,根据实际情况进行选择,例如需要频繁进行查找,很少插入和删除,宜选用顺序存储结构(数组),总是频繁插入删除,不怎么查询,选用链式存储结构。(面向应用)
单链表的优缺点官方套话:
存储分配方式:
单链表采用链式存储结构,用一组任意的存储单元存放元素(优点)
时间性能:
查找时间复杂度为O(n)(缺点)
插入和删除方便,时间复杂度为O(1)(优点)
空间性能:
存放元素时还需要另外开辟指针域空间(缺点)
动态分配空间,不需要 限定长度(优点)
经过我们上节的学习,知道了最简单的顺序存储的一些插入和删除的一些操作,顺序存储存储密度高,可以快速的存取任一位置的数据,但是我们同样也可以看到在插入和删除的过程中需要移动大量的数据(后面排队的人说,关我屁事,干嘛还要移动我)
为了解决顺序存储的问题,链式存储结构就出现了(通常使用指针来实现),顺序存储结构中,每个数据元素只需要存储数据信息,而链式存储呢还要存储后继元素的存储地址,通过这个地址就能找到后一个元素了(数据域+指针域)
二.单链表必备概念
(有头有尾)
链表中第一个结点(头节点)的存储位置叫做头指针(指针域)(头节点的数据域不存信息)
最后一个结点指针为NULL(尾节点的指针域为NULL)
头指针是链表的必要元素,必须不能为空。
头节点不是必要元素,为了操作统一,引入头节点(插入和删除等操作就和其他节点无差别)
若线性表为空表,则头节点的指针域为空。
(头结点的数据域可有可无,指针域必须有,头结点指针域=NULL就是空链表)
三.单链表相关算法:
数据结构是为了解决实际问题的,将各种复杂的操作都可以细化成一个个简单的算法操作,之后在通过不同的函数配合使用,解决问题,那么下面详细说一些常用的一些链表操作
(增,删,改,查,各种增,各种查等)
1.单链表的表示
从上面知识我们可以知道链表由若干个结点组成,每个结点又包含两个部分,数据域和指针域。
那么我们通过c语言进行转化过来,也很简单。
typedef int datatype; //方便替换数据类型
typedef struct node{
datatype data; //数据域
struct node *next;//指针域,指向下一个结点(下一结点也是listnode类型)
}listnode,*linklist;
上面的结构体我们可以理解为是一个结点,那么结点想要使用也需要占用内存,那么怎么为结构体申请内存空间。
一种是存储在栈上,一种是存储在堆上。
(存储在栈上)
listnode n1;
linklist p,q; //listnode *p,*q,两种表示方法都对
n1.data =10
n1.next =NULL;
p =&n1;//复习指针知识,P存储在栈上
p->data =20 //赋值方式
(存储在堆上)
q = (linklist)malloc(sizeof(listnode));//复习malloc知识,q存储在堆上
q->data = 30;
q->next =NULL;
free(q);
q = NULL; //避免野指针
2.单链表读取(没有顺序表查找方便)
需要从头遍历整个链表(工作指针后移),来查找,取出某一个元素。
我们有一个链表,里面有什么数据我们当然很关心啦。
思路就是:当指针域不为NULL时(尾结点的指针域为NULL),一直向下遍历。
void list_show(linklist H)
{
while(H->next) //循环判断指针域是不是NULL
{
printf("%d ",H->next->data);
H = H->next; //指针后移
}
}
如何获取指定 位置的结点?
linklist list_get(linklist H,int pos)
{
linklist p=H;
int i=-1;
if(pos<0) //说明输入的位置不合理
{
printf("position is invalid:<0\n");
return NULL;
}
while(p->next && i<pos)//是否遍历到表尾或者已经遍历到输入的结点
{
p=p->next;
i++;
}
//可能遍历到结点了,也有可能查询结点已经超出范围
if(i == pos)
{
return p;
}
else
{
printf("position is invalid: > length\n");
return NULL;
}
}
如何按照元素值进行查找??
linklist list_locate(linklist H,datatype value)
{
linklist p=H->next;
while(p&&p->data !=value) //遍历完链表都没找到,或者中途就已经找到
{
p=p->next;
}
return p;
}
3.单链表插入(链式存储的优势)
学习插入之前我们先建立一个空链表(指针域为NULL,数据域为0)
linklist list_create()
{
linklist H;
if((H=(linklist)malloc(sizeof(listnode)))==NULL) //1.为结构体分配空间
{
printf("malloc failed!\n");
return H;
}
H->data = 0; //2.数据域赋值为0
H->next = NULL; //3.指针域赋值为NULL
return H;
}
1> 头部插入
思路:
定义一个新结点p,分配内存
新结点的指针指向头结点指向的结点
头结点的指针域指向新结点
如图所示:
(1和2步骤不能整反了,不然H-->next指向的结点就丢失了)
int list_head_insert(linklist H,datatype value)
{
linklist p;
if((p=(linklist)malloc(sizeof(listnode)))==NULL) //1.申请空间
{
printf("malloc failed\n");
return -1;
}
p->data = value;
p->next = H->next;
H->next = p;
return 0;
}
2> 尾部插入
为了实现这个操作,我们定义了一个r结点(总是指向表尾,类似于一个表尾结点的影子(复制品))
- 建立一个空表H
- r=H;//初始状态下H为空表,此时它就是表尾结点,r复制一个H表尾结点
- p->next = NULL;//要插入的新结点P,它要作为表尾指针,所以指针域为NULL
- r->next = p; //相当于H->next 指向结点P
- r=p;//跟屁虫又来了,因为此时p是表尾了。
下面的例子是一个用户输入数值,将数值插入表尾的一个例子,思路就是上面的思路。
3> 单链表的按位置插入。
和其他的插入操作原理是一样的,主要就是找到插入位置的前一个结点。
int list_insert(linklist H,int pos,datatype value)
{
linklist p,q;
if(pos==0)
p=H;
else
p=list_get(H,pos-1); //查找到上一个结点位置
if(p==NULL)
{
printf("para is invalid\n");
return -1;
}
else
{
if((q=(linklist)malloc(sizeof(listnode)))==NULL)
{
printf("malloc failed\n");
return -1;
}
q->data = value;
q->next = p->next;
p->next = q;
return 0;
}
}
4> 单链表的有序插入
插入思想也是一样的。
关键:如何找到合适位置,如何找到前一个节点
一边遍历,遍历的同时进行了比较。
int list_order_insert(linklist H,datatype value)
{
linklist p,q;
if((p=(linklist)malloc(sizeof(listnode)))==NULL)
{
printf("malloc faied\n");
return -1;
}
p->data = value;
q=H;
//下一结点的值小于输入值时说明找到位置,或者它最大
while(q->next&&q->next->data < value)
{
q=q->next; //在循环遍历
}
p->next = q->next;
q->next = p;
return 0;
}
4. 单链表删除操作
皇帝--》大太子--》二太子--》三太子(死之前先记录下一个该谁继承了)
p-->next =p-->next->next(用q取代p-->next)
即:p-->next =q-->next;
1>特定编号位置的删除
注:如何找到位置的前一个结点?头结点操作特殊
int list_delete(linklist H,int pos)
{
linklist p,q;
if(pos == 0) //说明是头结点
p=H;
else
p=list_get(H,pos-1); //如果不是头结点,找到它的前一个结点
if(p==NULL || p->next==NULL) //说明查找失败
{
printf("para is invalid\n");
return -1;
}
else
{
q=p->next; //需要先存到q不然ai结点就丢失了。
p->next=q->next;
free(q);
q=NULL;
return 0;
}
}
5. 单链表倒置操作
自己理解了半天,就是倒不过来怎么操作的,想通了还是很好理解的
关键点:依次取链表中的各结点,将其作为新链表首结点插入到H结点之后。(取出一个头部插入,再取出一个头部插入)
遍历链表,提取结点,提取结点为新结点(看成新结点)进行头部插入
void list_reverse(linklist H)
{
linklist p,q;
p=H->next; //原链表一分为二
H->next = NULL;
while(p)
{
q=p; //q暂存p的位置(此时p就可以后移动了)
p=p->next; //p负责向后移动来遍历链表
q->next = H->next; //下面就是个头部插入的操作。
H->next = q;
}
}
6. 单链表的排序
冒泡排序等适合顺序存储(数组)操作
单链表排序一般为直接插入排序
void list_sort(linklist H)
{
linklist p,q,r;
p=H->next;
H->next = NULL;
while(p)
{
q=p;
p=p->next;
r=H;
while(r->next && r->next->data < q->data)
r=r->next; //r来进行循环遍历
q->next = r->next;//与头部插入操作是一样的,这是r后插入
r->next = q;
}
}
常用的操作基本梳理了一遍,这次理解了,但是还是需要不断的复习,不然看到还是会蒙。
上一篇: 冬季润燥喝蜜奶养生效果最好!蜂蜜用处多!
下一篇: 校园里的孩子都很雷