学习数据结构知识的同时,梳理知识,也便于以后查找
tags:《数据结构(c语言版)》、B站视频
线性表、单链表
线性表
线性结构是一个数据元素的有序(次序上的)集。
线性表数据结构中的一种,是由n个类型的数据元素组成的有限序列,具有前驱和后继,非空线性表还具有开始结点和终端结点。
在稍复杂的线性表中,一个数据元素可以由若干个数据项
组成。在这种情况下,常把数据元素称为记录
,含有大量记录的线性表又称为文件
。
线性表的两种实现方式
顺序表示(顺序表)
-
概念:用一组地址
连续的存储单元
依次存储线性表的数据元素,这种存储结构的线性表称为顺序表。 -
特点:逻辑上相邻的数据元素,物理次序也是相邻的。
只要确定好了存储线性表的起始位置,线性表中任一数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的储存结构,因为高级语言中的数组类型也是有随机存取的特性,所以通常我们都使用数组来描述数据结构中的顺序储存结构,用动态分配的一维数组表示线性表。
设线性表中每个元素需占用l
个存储单元,则信息必须中元素满足:LOC(a(i+1)) = LOC(ai) + l
,;第i
个数据元素ai
的存储位置LOC(ai) = LOC(a1) + (i-1)* l
。第一个数据元素的地址被称为基地址
线性表的这种机内表示
称做线性表的顺序存储结构或顺序映像
。换句话说,以元素在计算机内物理位置相邻
来表示线性表中数据元素之间的逻辑关系
代码实现
以最简单的学生信息管理为例:
- 首先先创建两个数据结构,如下
#define maxsize 100 //定义学生最大数量
#define OK 1 //正确标志
#define ERROR 0 //失败标志
//学生信息的数据结构
typedef struct
{
int id; //学生id
char name[30]; //学生姓名
}Student;
//顺序表数据结构
typedef struct
{
Student *elem; //储存空间的基地址
int length; //数据结构的长度
}SqList;
//定义SqList类型的变量
SqList L;
这是一个十分简单的例子,这样我们就可以通过L.elem[i-1]访问序号为i的学生信息了。其实这里我们用到了指针数组。
- 基本算法
//初始化,顺序表基本算法
Status InitList(SqList &L)
{
//构造一个空的顺序表L
L.elem = new ElemType[maxsize]; //分配内存空间
if(!L.elem) exit(-1);
L.length = 0;
return OK;
}
//顺序表取值
Status Get(SqList &L,int i,ElemType &e)
{
if(i<1||i>L.length) return ERROR;
e = L.elem[i-1];
return OK;
}
//查找,顺序表查找
int Find(SqList L,ElemType e)
{
//查找值为e的数据元素,返回其序号
for(i=0;i<L.length;i++)
{
if(L.elem[i]==e) return i+1;
}
return ERROR; //查找失败
}
//插入,顺序表插入
Status ListInsert(SqList &L,int i,ElemType e)
{
if((i<1)||(i>L.length+1)) return ERROR; //i不合法
if(L.length == maxsize) return ERROR; //满了
for(j=L.length-1;j>=i-1;j--)
L.elem[j+1]=L.elem[j]; //将第n个至i个位置的元素后移
L.elem[i-1]=e; //将e放进第i个位置
}
//删除,顺序表删除
Status ListDelete(SqList &L,int i)
{
//删除第i个元素,i的值为[1,L.length]
if((i<1)||(i>L.length)) return ERROR;
for(j=i;j<=L.length-1;j++)
L.elem[j-1]=L.elem[j];
--L.length; //长度减一
return OK;
}
算法分析
- 算法都十分的简单,但为啥有的参数用的是引用,有的不是呢?
这里讲下使用引用作为形参的作用了,主要有三点:
- 使用引用作为参数与使用指针作为参数的效果是一样的,形参变化时实参对应也会变化,这个我在上篇文章(我上面给的链接)也有说明,引用只是一个别名。
- 引用类型作为形参,在内存中并没有产生实参的副本,而使用一般变量作为形参,,形参和实参会分别占用不同给的存储空间,当数据量较大时,使用变量作为形参可能会浪费时间和空间。
- 虽然使用指针也可以达到引用一样的效果,但是在被调函数中需要重复使用"*指针变量名"来访问,很容易产生错误并且使程序的阅读性变差。
此时你会发现,使用顺序表作为存储时,空间是一次性直接开辟的,所以可能会有空间不足或者浪费空间的情况出现,那么为啥不用一个就分配一个空间呢,再使用一个方式将这些空间串起来不就好了,是时候展现真正的技术了(链表)。
线性表的链式表示和实现
概念:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的),包括数据域和指针域,数据域存数据,指针域指示其后继的信息
。
单链表可由头指针唯一确定。
由于最后一个元素没有直接后继
,则线性链表最后一个结点的指针为空(NULL)
。
用线性链表表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的。
线性链表
- 没有头结点的单链表
- 带头结点的单链表,头指针指向头结点的非空指针
- 对第一个结点的操作不再特殊
- 空表和非空表的处理也就统一
头指针、头结点和首元结点
- 头指针是指向链表的第一个结点(或为头结点或为首元结点)的指针
- 单链表可由头指针唯一确定
- 头结点是在链表的首元结点之前附设的一个结点,数据域只存放空表标准和表长等信息
- 首元结点是链表在存储线性表第一个数据元素a1的结点
头指针->头结点->首元结点
代码实现
- 合并单链表。按书上的合并两个单链表为一个有序链表
//书上,合并两个单链表为一个有序链表
void MergeList_L(LinkList &La, LinkList &Lb, LinkList &Lc ){
/**
* 单链表La、lb元素按值非递减排列
* 归并单链表La、lb得到单链表Lc,lc的元素按值非递减排列
*/
pa = La->next; pb = Lb->next;
//用la作Lc的头结点
Lc = pc = La;
while(pa && pb){
if(pa->data <= pb->next){
pc->next = pa; pc = pa; pa = pa->next;
}
else{
pc->next = pb; pc = pb; pb = pb->next;
}
}
//插入剩余段
pc->next = pa ? pa : pb;
//释放Lb的头结点
free(Lb)
}//MergeList_L
- 完整代码
#include<stdio.h>
//存储结构
typedef int ElemType;//起一个别名
typedef struct node{//定义链表结点类型
ElemType data;//数据域
struct node *next;//指针域
}LNode,slink,*LinkList; //单链表类型名
//头插法
LNode *Creatslink_Head(int n){
LNode *head,*p;
int i;
if(n<1) return NULL;
head = NULL;
for( i = 1; i <= n; i++)//建立n个结点的单链表
{
p = (LNode *)malloc(sizeof(LNode));
if(p == NULL){//内存申请失败,错误处理
printf("内存申请失败!");
return 0;
}
scanf("%d",&p->data);
p->next = head;
head = p;
}
p = (slink*)malloc(sizeof(slink));
if(p == NULL){//内存申请失败,错误处理
printf("内存申请失败!");
return 0;
}
p->next = head;
head = p;//头结点
return (head); //返回头指针
}
//尾插法/*增加一个尾指针*/
LNode *Creatslink_End(int n){
LNode *head,*p,*r;
if(n<1) return NULL;
int i;
r = head = (LNode *)malloc(sizeof(LNode));//头结点
if(r == NULL){//内存申请失败,错误处理
printf("内存申请失败!");
return 0;
}
for( i = 1; i <= n; i++)//建立n个结点的单链表
{
p = (LNode *)malloc(sizeof(LNode));
if(p == NULL){//内存申请失败,错误处理
printf("内存申请失败!");
return 0;
}
scanf("%d",&p->data);
r->next = p; r = p;
}
r->next = NULL;//处理尾结点
return (head); //返回头指针
}
//按序号查找
LNode *LocateNumber(LNode *head,int i){//返回第i个结点的指针
int j = 1;
LNode *p;
p = head->next;//让p指向第一个结点
while (p->next && j < i)//下一结点不为空,且没到i
{
p = p->next;
j++;
}
if(i == j){
return p;
}else
{
return NULL;
}
}
//按值查找
LNode *LocateValue(LNode *head,ElemType x){
LNode *p;
p = head->next;//让p指向第一个结点
while (p && p->data != x)
{
p = p->next;
}
if (p == NULL)//找到最后一个结点的指针为空
{
return 0;
}
return p;
}
//插入
LNode *Insert(LNode *head,int i,ElemType x){
//在第i个结点之前插入值为x的新结点
LNode *p,*q;
int j = 0;
if(i < 1) return 0;
q = head;
while(p && j < i-1){//找第i-1个结点
p = p->next;j++;
}
if(p == NULL) return 0;//i值超过表长+1
q = (LNode *)malloc(sizeof(LNode));
if(p == NULL){//内存申请失败,错误处理
printf("内存申请失败!");
return 0;
}
q->data = x;
q->next = p->next;
p->next = q;
return 1;
}
//删除
LNode *DeleteNode(LNode *head,int i,ElemType *e){
//删除单链表中第i个结点
slink *p,*q;
int j = 0;
p = head;
while(p->next && j < i-1){
p = p->next;
j++;
}
if(p->next == NULL || j > i-1){//当i>n或i<1时,删除位置不合理
return 0;
}
q = p->next;//q指向被删除结点
p ->next = q->next;
*e = q->data;
free(q);
return 1;
}
//输出链表
void ShowLinklist(LNode *head)
{
LNode *p;
p = head;
int len = LinkList_length(head);
printf("单链表长度: "); printf("%d\n",len);
while(p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
int LinkList_length(LNode *head){//计算链表长度
int length=0;
LNode *p;
p = head;
while(p->next){
p = p->next;
length++;
}
length /= 2;
return length;
}
//查找链表中倒数第k个位置上的结点(k为正整数)。
//若查找成功,算法输出该结点的data值,并返回1;
//否则,只返回0。
int Search(LNode *head,int k){
LNode *p;
p = head;
int len = LinkList_length(p);
int i = 0;//计数器
while (p->next)
{
if(len-k == i+1){
printf(p->data);
return 1;
}
i++;
}
if(p->next == NULL || i > len-1){//当i>n或i<1时,删除位置不合理
return 0;
}
return 0;
}
//高效算法
//该链表只给出了头指针list。在不改变链表的前提下,尽可能快
int HighSearch(LNode *list,int x){
int len = LinkList_length(list);//获得链表长度
if(x > len) return 0;
LNode *q,*p;
int num = 0;//计算器
q = p = list->next;//让p、head指向第一个结点
while (p && p->next == NULL)//p指针达到链表尾
{
if(num >= x){
q = q->next;
}
p = p->next;
num++;
}
return 0;
}
//使用双指针,实现就地逆置
void Inverse(LNode *head) {//就地逆置
LNode *p, *q;
p = head->next;
head->next = NULL;
while (p != NULL) {
q = p;
p = p->next;
q->next = head->next;
head->next = q;
}
}
int main(void){
int choice;
LNode *head;
//head = (linklist*)malloc(sizeof(linklist));
while(1)
{
printf("单链表的创建\n");
printf("1.使用尾插法创建单链表\n");
printf("2.就地逆置\n");
printf("3.链表输出显示\n");
printf("4.退出\n");
printf("做出选择:\n");
scanf("%d",&choice);
switch(choice)
{
//尾插法
case 1:
head = CreateList_End();
break;
//就地逆置
case 2:
Inverse(head);
break;
//输出链表
case 3:
ShowLinklist(head);
break;
//退出程序
case 4:
return 0;
break;
default:
break;
}
}
return 1;
}
使用游标(指示器cur),这种用数组描述的链表叫
静态链表
循环链表
特点:
表中最后一个结点的指针域指向头结点,整个链表形成一个环。
从表中任一结点出发均可找到表中其他结点。
图示
相关操作:
- 循环链表的操作与线性链表基本一致,差别仅在于算法中的循环条件不是p或p->next是否为空,而是它们是否等于头指针。
循环链表
为了克服查找单链表的单向性的缺点(查找结点的直接前趋,需从头指针出发,执行时间O(n)
),有了双向链表。
双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱。
代码描述
//存储结构
typedef int ElemType;//起一个别名
typedef struct DuLNode{
ElemType data;
struct DulNode *prior;
struct DulNode *next;
}DuLNode, *DuLinkList;
双向链表
和单链表类似,双向链表也可以有循环表
由于链表在空间的合理利用上,和插入、删除时不需要移动等优点。在很多场合下,是线性表的事选存储结构。
插入算法实现
//插入
Status ListInsert_DuL(DuLinkList &L,int i,ElemType e){
//在双向链表L第i个位置之前插入元素e
//i的合法值为1 <= i <= 表长+1
if(!p = GetEIemP_DuL(L,i)) return ERROR;
if(!(s = (DuLinkList)malloc(sizeof(DuLNode)))) return ERROR;
s->data = e;
s->prior = p->prior; p->prior->next = s;
s->next = p; p->prior = s;
return OK;
}//ListInsert_DuL
删除算法实现
//删除
Status ListDelete_DuL(DuLinkList &L,int i,EIemType &e){
//删除双向链表L第i个位素
//i的合法值为1 <= i <= 表长+1
if(!p = GetEIemP_DuL(L,i)) return ERROR;
e = p->data;
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
return OK;
}//ListDelete_DuL
–END–