数据结构(一)- 线性表

本文详细解读了数据结构与算法的概念,包括数据、数据元素、数据对象、逻辑结构(集合、线性、树形、图形)、物理结构(顺序与链式)、算法特性与设计要求,重点介绍了线性表(顺序与链式)的操作及其优缺点,涵盖了单链表、循环链表和双向链表。深入探讨了时间复杂度和空间复杂度的度量,以及常用的数据结构和算法实例。
摘要由CSDN通过智能技术生成

《大话数据结构》学习笔记

一、数据结构与算法概念

数据结构:是相互之间存在一种或多种特定关系的数据元素的集合

  • 数据:是描述客观事物的符合,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符合集合
  • 数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理

如在禽类中,牛、马、羊就是禽类的数据元素。

  • 数据项:一个数据元素可以有若干个数据项组成

如人的数据元素,可以有眼睛、头发、耳朵这些数据项。

  • 数据对象:是性质相同(指相同数量和类型的数据项)的数据元素的集合,是数据的子集

1. 逻辑结构和物理结构

1.1 逻辑结构

指数据对象中数据元素之间的相互关系,可分为以下四种

(1)集合结构

集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。

在这里插入图片描述

(2)线性结构

线性结构中的数据元素之间是一对一的关系。
在这里插入图片描述

(3)树形结构

树形结构中的数据元素之间存在一对多的层次关系。

在这里插入图片描述

(4)图形结构

图形结构中的数据元素之间多对多的关系。

在这里插入图片描述

1.2 物理结构

指数据的逻辑结构在计算机中的存储形式。大意就是如何把数据元素存储到计算机中的存储器(主要针对内存而言)中。

而数据的存储结构应正确的反映数据元素之间的逻辑关系,数据元素的存储结构形式有两种:

(1)顺序存储结构

是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的,如数组。

在这里插入图片描述

(2)链式存储结构

是把数据元素存放在地址任意的存储单元里,存储单元可以是连续的,也可以是不连续的

在这里插入图片描述

2. 算法

算法是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作

2.1 算法特性
  • 输入和输出:算法具有零个或多个输入,至少有一个或多个输出
  • 有穷性:指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成
  • 确定性:算法的每一步骤都具有确定的含义,不会出现二义性
  • 可行性:算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成
2.2 算法设计要求
  • 正确性:指算法至少应该具有输入、输出和加工处理无歧义性、能正确反映问题的需求、能够得到问题的正确答案
  • 可读性:算法设计的另一目的是为了便于阅读、理解和交流
  • 健壮性:当输入数据不合法时,算法也能做出相关处理,而不是产生异常或莫名其妙的结果
  • 算法设计应该尽量满足时间高效率和存储量低的需求
2.3 算法效率的度量

(1)事后统计方法

这种方法主要是通过设计好的测试程序和数据,利用计算机计时器对不同算法编制的程序的运行时进行比较,从而确定算法效率的高低。

缺点:

  • 必须依据算法事先编制好程序,这通常需要花费大量的时间和精力。
  • 时间的比较依赖计算机硬件和软件等环境因素,有时会掩盖算法本身的优劣。
  • 算法的测试数据设计困难。

因此,这种方法我们考虑不采纳

(2)事前分析估算方法

在计算机程序编制前,依据统计方法对算法进行估算。

经过分析和发现,一个用高级程序语言编写的程序在计算机上运行时所消耗的时间取决于下列因素:

  • 算法采用的策略、方法。
  • 编译产生的代码质量。(软件支持)
  • 问题的输入规模。
  • 机器执行指令的速度。(硬件性能)

因此,抛开与计算机硬件、软件有关的因素,一个程序的运行时间,依赖于算法的好坏和问题的输入规模(输入量的多少)

2.4 算法时间复杂度

判断一个算法的效率时,函数中的常数和其他次要项常常可以忽略,而更应该关注最高的阶项的阶数

定义:在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n) = O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。

这样用大写O()来体现算法时间复杂度的记法,称之为大O记法。一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法

大O阶推导方法:

  1. 用常数1取代运行时间中的所有加法常数
  2. 在修改后的运行时间次数函数中,只保留最高阶项
  3. 如果最高阶项存在且不是1,则去除与这个项相乘的常数
  4. 得出的结果就是大O阶

下面通过几个常用例子

(1)常数阶

int sum = 0, n = 100;
sum = (1+n)*n/2;
sum = (1+n)*n/2;
sum = (1+n)*n/2;
sum = (1+n)*n/2;
sum = (1+n)*n/2;
sum = (1+n)*n/2;
sum = (1+n)*n/2;
sum = (1+n)*n/2;
//运行次数:9
//时间复杂度:O(1)

这种与n的多少(问题规模)无关、执行时间恒定的算法,称之为具有O(1)的时间复杂度,又叫常数阶。

注意:无论这个常数多少,都记作O(1)

(2)线性阶

for(int i = 0; i < n; i++){
    printf("执行 %d 次", i);
}
//运行次数:n
//时间复杂度:O(n)

(3)对数阶

int count = 1;
while(count < n){
    count *= 2;
}
//运行次数:2^x + 1
//时间复杂度:O(logn)

由于每次count乘以2之后,就距离n更近一分。也就是说,有多少个2相乘后大于n,则会退出循环。

由2x = n得到x = log2n 。所以时间复杂度为O(logn)

(4)平方阶

for(int i = 0; i < n; i++){
   for(int j = 0; j < n; j++){
    	printf("---");
	} 
}
//运行次数:n^2
//时间复杂度:O(n^2)

下表是常见的时间复杂度:

执行次数函数非正式术语
12O(1)常数阶
2n+3O(n)线性阶
3n2+2n+1O(n2)平方阶
5log2n+20O(logn)对数阶
2n+3nlog2n+19O(nlogn)nlogn阶
6n3+2n2+3n+4O(n3)立方阶
2nO(2n)指数阶

常用的时间复杂度所耗费的时间从小到大依次是:
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

  • 最坏情况运行时间:指一种保证,表示运行时间将不会再坏了。在应用中,这是一种最重要的需求,通常,除非特别指定,我们提到的运行时间都是最坏情况的运行时间
  • 平均运行时间:是所有情况中最有意义的,因为它是期望的运行时间
  • 算法空间复杂度:通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中n为问题规模,f(n)为语句关于n所占存储空间的函数

二、线性表

线性表(List):零个或多个数据元素的有限序列

在这里插入图片描述

线性表元素的个数n(n>=0)定义为线性表的长度,当n=0时,称为空表

线性表的抽象数据类型的基本操作如下:

InitList(*L); 			//初始化操作,建立一个空的线性表L
ListEmpty(L); 			//若线性表为空,返回true,否则返回false
ClearList(*L); 			//将线性表清空
GetElem(L,e);			//将线性表L中的第i个位置元素值返回给e
LocateElem(L,e);		//在线性表L中查找与给定值e相等的元素,如果查找成功,返回该元素在表中的序号表示成	
						//功;否则,返回0表示失败
ListInsert(*L,i,e);		//在线性表L中的第i个位置插入新元素e
ListDelete(*L,i,*e);	//删除线性表L中的第i个位置元素,并用e返回其值
ListLength(L);			//返回线性表L的元素个数

1. 线性表的顺序存储结构

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素

我们可以用一维数组来实现顺序存储结构:

#define MAXSIZE 20 //存储空间初始分配值
typedef int ElemType; //自定义类型别名
typedef struct{
    Elemtype data[MAXSIZE]; //数组存储空间的起始位置,数组长度为MAXSIZE
    int length; //线性表当前长度
} SqList;
  • 数组长度是存放线性表的存储空间的长度,存储分配后这个量一般是不变的

  • 线性表的长度是线性表中数据元素的个数,随着插入和删除操作,这个量是可变的

  • 在任意时刻,线性表长度应该小于等于数组长度

  • 用数组存储顺序表意味着要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,因此分配的数组空间要大于等于当前线性表的长度

存储器中的每个存储单元都有自己的编号,并且可以通过一个公式,能随时算出线性表中任意位置的地址,那么在对每个线性表位置的存入或者取出数据,对于计算机来说都是相等的时间,也就是一个常数。因此,它的存取时间性能在时间复杂度上为O(1)。我们通常把具有这一特点的存储结构称为随机存取结构

优点:

  • 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
  • 可以快速地存取表中任意位置的元素

缺点:

  • 插入和删除操作需要移动大量元素
  • 当线性表长度变化较大时,难以确定存储空间的容量
  • 造成多余的存储空间
1.1 获得元素操作

GetElem():实现将线性表L中的第i个位置元素值返回

//初始条件:顺序线性表L已存在,1 <= i <= ListLength(L)
# define OK 1
# define ERROR 0
# define TRUE 1
# define FALSE 0
typedef int Status; //将作为函数返回类型,表示函数结果状态码,如OK、ERROR
Status GetElem(SqList L, int i, ElemType *e){
    if(L.length == 0 || i < 1 || i > L.length){
        return ERROR;
    }
    *e = L.data[i-1];
    return OK;
}
1.2 插入操作

ListInsert():实现在线性表L中的第i个位置插入新元素e

插入算法的思路:

  • 如果插入位置不合理,抛出异常。
  • 如果线性表长度大于等于数组长度,则抛出异常或动态增加容器。
  • 从最后一个元素开始向前遍历到第i个位置,分别将它们都向后移动一个位置。
  • 将要插入元素填入位置i处。
  • 表长加1。
//初始条件:顺序线性表L已存在,1 <= i <= ListLength(L)
Status ListInsert(SqList *L, int i, ElemType e){
    if(L->length == MAXSIZE){
        return ERROR; //顺序线性表已经满了
    }
    if(i < 1 || i > L->length+1){
        return ERROR; //i不在范围内
    }
    if(i <= L->length){//若插入数据位置不在表尾
        for(int k = L->length-1; k >= i-1; k--){ //将要插入位置后数据元素向后移动一位
            L->data[k+1] = L->data[k];
        }
    }
    L->data[i-1] = e; //将新数据插入
    L->length++; //更新线性表当前长度
    return OK;
}
1.3 删除操作

ListDelete():实现删除线性表L中的第i个数据元素,并用e返回其值

删除算法的思路:

  • 如果删除位置不合理,抛出异常。
  • 取出删除元素。
  • 从删除元素位置开始遍历到最后一个元素位置,分别将它们都向前移动一个位置。
  • 将要插入元素填入位置i处。
  • 表长减1。
//初始条件:顺序线性表L已存在,1 <= i <= ListLength(L)
Status ListDelete(SqList L, int i, ElemType *e){
    if(L->length == 0){
         return ERROR; // 线性表为空
    }
    if(i < 1 || i > L->length){
        return ERROR; //删除位置不正确
    }
    *e = L->data[i-1];
    if(i < L->length){ //如果删除位置不是最后位置
        for(int k = i; k < L->length; k++){ //将删除位置后继元素前移
            L->data[k-1] = L->data[k];
        }
    }
    L->length--; //更新线性表当前长度
    return OK;
}

2. 线性表的链式存储结构

线性表的顺序存储结构最大的缺点就在于插入和删除时需要移动大量元素,这是非常耗时的。为解决这一问题,链式存储结构应运而生。以前的顺序结构中,每个数据元素只需要存数据元素信息,而在链式结构中,除了要存数据元素信息外,还要存储它的后继元素地址。

我们把存储数据元素信息的域称为数据域,把存储后继元素位置的域称为指针域。指针域中存储的信息称为指针或链。由数据源和指针域构成的数据元素的存储映像则称为结点(Node)

n个结点链结成一个链表,即为线性表的链式存储结构,而每个结点只包含一个指针域的链表,称为单链表

我们把链表中第一个结点的存储位置叫做头指针,整个链表的存取就必须是从头指针开始进行,之后的每一个结点就是上一个的后继指针指向的位置,那么显然最后一个结点中的后继指针为NULL,因为最后结点的后继就是不存在。为了方便对链表的进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头节点的数据域可以不存储任何数据信息,但可以存储线性表的长度等附加信息,头节点的后继指针指向第一个结点。

在这里插入图片描述

头指针:

  • 指链表指向第一个结点的指针,若链表有头结点,则是指向头结点的指针
  • 头指针具有标识作用,所以常用头指针冠以链表的名字
  • 无论链表是否为空,头指针均不为空。头指针是链表的必要元素

头结点:

  • 头结点是为了操作的统一和方便而设立的,放在第一个元素的结点之前,其数据域一般无意义(也可存放链表的长度)
  • 头结点不一定是链表必须要素

线性表的单链表存储结构:

typedef int ElemType; 
struct Node{ //定义结点
    ElemType data; //数据域
    struct Node *next; //指针域
};
typedef struct Node *LinkList;
2.1 单链表的读取

GetElem():实现单链表获取第i个元素的数据

算法思路:

  • 声明一个指针p指向链表第一个结点,初始化j从1开始
  • j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
  • 若到链表末尾p为空,则说明第i个结点不存在
  • 否则查找成功,返回结点p的数据
//初始条件:链表L已存在
# define OK 1
# define ERROR 0
# define TRUE 1
# define FALSE 0
typedef int Status; //将作为函数返回类型,表示函数结果状态码,如OK、ERROR

Status GetElem(LinkList L, int i, ElemType *e){
    int j;
    LinkList p; //声明指针p
    p = L->next; //让p指向链表第一个结点
    j = 1; //j为计数器
    while((p != NULL) && (j < i)){ //p不为空且j不大于等于i时,循环继续
        p = p->next; // p指向下一个结点
        j++; //计数器累加
    }
    if(!p || j > i){ //p为空或j大于i时返回错误代码
        return ERROR;
    }
    *e = p->data; // 取第i个结点的数据
    return OK;
}
2.2 单链表的插入

假设有结点pp->next,若要把存储元素e的结点s插入到结点pp->next之间,只需要让s->nextp->next的指针发生改动即可。

s->next = p->next;
p->next = s;

在这里插入图片描述

ListInsert():实现单链表在第i个结点位置前插入新数据e

算法思路:

  • 声明一指针p指向链表头结点,初始化j从1开始
  • j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
  • 若到链表末尾p为空,则说明第i个结点不存在
  • 否则查找成功,在系统中生成一个空结点s
  • 将数据元素e赋值给s->data
  • 单链表的插入标准语句s->next = p->next; p->next = s;
//初始条件:链表L已存在
Status ListInsert(LinkList *L, int i, ElemType e){
    int j;
    LinkList p, s;
    p = *L; //把p指向头结点
    j = 1;
    while(p && j < i){ //寻找第i-1个结点(头结点不是第一个结点)
        p = p->next;
        j++;
    }
    if(!p || j > i){ //p为空或j大于i时返回错误代码,表示结点不存在
        return ERROR;
    }
    s = (LinkList)malloc(sizeof(struct Node)); //开辟动态空间,生成新结点
    s->data = e; //将新数据赋值给新结点数据域
    s->next = p->next; //将p的后继指针赋值给s结点的后继
    p->next = s; //将s赋值给p的后继
    return OK;
}
2.3 单链表的删除

删除更简单,假设有结点pp->nextp->next->next,若要删除结点p->next,只需改动p->next的指针即可,为了节省空间,我们可以定义一个临时变量q用于释放要删除结点的内存空间。

q = p->next;
p->next = q->next;
free(q);

在这里插入图片描述

ListDelete():实现单链表删除第i个结点

算法思路:

  • 声明一指针p指向链表头结点,初始化j从1开始
  • j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
  • 若到链表末尾p为空,则说明第i个结点不存在
  • 否则查找成功,将欲删除的结点p->next赋值给q
  • 单链表的删除标准语句p->next = q->next;
  • q结点的数据赋值给e,作为返回
  • 释放q结点
//初始条件:链表L已存在
Status ListInsert(LinkList *L, int i, ElemType e){
    int j;
    LinkList p, q;
    p = *L; 
    j = 1;
    while(p->next && j < i){
        p = p->next;
        j++;
    }
    if(!(p->next) || j > i){ //p为空或j大于i时返回错误代码,表示结点不存在
        return ERROR;
    }
    q = p->next; //将p的后继结点赋值给q
    p->next = q->next; //将q的后继结点赋值给p的后继
    *e = q->data;
    free(q); //释放结点内存空间
    return OK;
}
2.4 单链表的整表创建

对于每个链表来说,它所占用空间的大小和位置是不需要预先分配划定的,可以根据系统的情况和实际的需求即时生成。所以创建单链表的过程就是一个动态生成链表的过程。

CreateListHead():实现创建单链表L(头插法)

算法思路:

  • 声明一指针p和计数器变量i
  • 初始化一空链表L
  • L的头结点的指针指向NULL,即建立一个带头结点的单链表
  • 循环
    • 生成一新结点赋值给p
    • 随机生成一数字赋值给p的数据域p->next
    • p插入到头结点于前一个结点之间
void CreateListHead(LinkList *L, int n){
    LinkList p;
    int i;
    srand(time(0)); //初始化随机数种子
    *L = (LinkList)malloc(sizeof(struct Node));
    (*L)->next = NULL; //建立带头结点的单链表
    for (i = 0; i < n; i++){
        p = (LinkList) malloc(sizeof(struct Node)); //生成新结点
        p->data = rand() % 100 + 1;
        p->next = (*L)->next; 
        (*L)->next = p; //插入到表头
    }
}

这种头插法就是始终让新结点在第一的位置。然而我们按照正常思维,都是把新结点插到后面的,这种方法称为尾插法。

CreateListTail():实现创建单链表L(尾插法)

void CreateListTail(LinkList *L, int n){
    LinkList p, r;
    int i;
    srand(time(0)); //初始化随机数种子
    *L = (LinkList)malloc(sizeof(struct Node));
    r = *L; //r指向链表尾结点
    for (i = 0; i < n; i++){
        p = (LinkList) malloc(sizeof(struct Node)); //生成新结点
        p->data = rand() % 100 + 1;
        r->next = p; //将新结点赋给尾结点的后继
        r = p; //r指向新的尾结点
    }
    r->next = NULL; //尾结点后继指向NULL
}
2.5 单链表的整表删除

所谓删除,就是释放所有内存空间。

ClearList():实现单链表L删除

算法思路:

  • 声明一个结点pq
  • 将第一个结点赋值给p
  • 循环:
    • 将下一个结点赋值给q
    • 释放p
    • q赋值给p
//初始条件:链表L已存在
Status ClearList(LinkList *L){
    LinkList p, q;
    p = (*L)->next; //p指向第一个结点
    while(p){ //循环至表尾
        q = p->next; //q指向p的后继结点
        free(p); //释放p指向结点的内存空间
        p = q; //p重新指向q(原p的后继结点)
    }
    (*L)->next = NULL; //头结点指针域为空
    return OK;
}
2.6 单链表结构与顺序结构优缺点

存储分配方式:

  • 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
  • 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素

时间性能:

  • 查找
    • 顺序存储结构O(1)
    • 单链表O(n)
  • 插入和删除
    • 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
    • 单链表在线出某位置的指针后,插入和删除时间仅为O(1)

空间性能:

  • 顺序存储结构需要预分配存储空间,分大了浪费,分小了易发生上溢
  • 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制

结论:

  • 若线性表需要频繁查找,很少进行插入和删除操作时,宜采用顺序存储结构。若需要频繁插入和删除时,宜采用单链表结构
  • 当线性表中的元素个数变化较大或者根本不知道有多大时,最好采用单链表结构,这样可以不用考虑存储空间的大小问题
2.7 静态链表

利用数组描述的链表叫做静态链表,这种描述方法还有起名叫做游标实现法。数组元素由两个数据域组成,datacur。数据域data用来存放数据元素,而cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,所以cur又称为游标。

线性表的静态链表存储结构:

//为了方便插入数据,通常会把数组建立得大一些,以便有一些空闲空间可以便于插入时不至于溢出。
#define MAXSIZE 1000
typedef int ElemType;
typedef struct{
    ElemType data;
    int cur; // 游标,为0时表示无指向
} Component, StaticList[MAXSIZE];

通常把未被使用的数组元素称为备用链表,而数组第一个元素(下标为0)的cur存放备用链表第一个结点的下标。数组最后一个元素的cur则存放第一个有数值的元素(已使用)的下标,相当于单链表中的头结点,当链表为空时则为0。

在这里插入图片描述

初始化数组状态代码如下:

Status InitList(StaticLinkList space){//数组名即为数组首元素的地址
    for(int i = 0; i < MAXSIZE - 1; i++){
        space[i].cur = i + 1;
    }
    space[MAXSIZE-1].cur = 0; //目前静态链表为空,最后一个元素的cur为0
    return OK;
}

假设我们已经将数据存入静态链表,来实现插入、删除操作,如下图:

在这里插入图片描述

插入操作

在静态链表中要解决的是:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放

而在静态链表中,由于操作的是数组,无法使用malloc()free()两个函数,因此我们需要自己实现这两个函数。

为了辨明数组中哪些分量未被使用,解决办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。

替代malloc()的代码如下:

//若备用链表非空,则返回分配的结点下标,否则为0
int Malloc_SLL(StaticLinkList space){
    int i = space[0].cur;//返回的第一个备用空闲的下标
    if(space[0].cur){
        space[0].cur = space[i].cur; //将下一个备用空闲下标作为第一个备用空闲下标
    }
    return i;
}

现我们实现在之间,插入一个值为的新元素。以前在数组中我们采用元素后移策略来完成插队,现在则不需要了,我们从备用链表中获得一个空位置,然后通过cur定位的方式完成插队。如图所示:

在这里插入图片描述

//在L中第i个元素之前插入新的数据元素e
Status ListInsert(StaticLinkList L, int i, ElemType e){
    int j, k;
    k = MAX_SIZE - 1; //一开始的值为最后一个元素的下标
    if(i < 1 || i > ListLength(L) + 1){ 
        return ERROR;
    }
    j = Malloc_SSL(L); //获得空闲分量的下标
    if(j){
        L[j].data = e; //数据赋值
        for(int l = 1; l <= i - 1; l++){ //找到第i个元素之前的位置
            k = L[k].cur;
        }
        L[j].cur = L[k].cur; //把第i个元素之前的cur赋值给新元素的cur
        L[k].cur = j; //把新元素的下标赋值给第i个元素之前元素的cur
        return OK;
    }
    return ERROR;
}

删除操作

同样的,在进行删除操作前,我们还得先实现释放结点的方法。

替代free()的代码如下:

//将下标为k的空闲结点回收到备用链表
void Free_SSL(StaticeLinkList space, int k){
    space[k].cur = space[0].cur; //把第一个元素cur值赋给要删除分量的cur
    space[0].cur = k; //把要删除的分量下标赋值给第一个元素的cur
}

现实现删除甲元素,如图所示:

在这里插入图片描述

//删除在L中第i个数据元素e
Status ListDelete(StaticLinkList L, int i){
    int j, k;
    if(i < 1 || i > ListLength(L)){
        return ERROR;
    }
    k = MAX_SIZE - 1;
    for(j = 1; j <= i - 1;j++){ //找到第i个元素之前的位置
        k = L[k].cur; 
    }
    j = L[k].cur; //将i(删除元素)的坐标赋值j
    L[k].cur = L[j].cur; //断开删除元素与相邻元素之间的连接,并且把相邻元素连接上
    Free_SSL(L, j); //释放删除元素空间,同时将删除元素作为备选链表首元素
    return OK;
}

静态链表的其他相关操作:获取静态链表数据元素个数

//初始条件:静态链表L已存在,返回值为L中数据元素个数
int ListLength(StatucLinkList L){
    int j = 0;
    int i = L[MAXSIZE-1].cur; //获取首元素下标
    while(i){//循环获取下一个元素坐标,并且计数,直到为0,表示没有下一元素
        i = L[i].cur;
        j++;
    }
    return j;
}

静态链表优点:

  • 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和和删除操作需要移动大量元素的缺点

静态链表缺点:

  • 没有解决连续存储分配带来的表长难以确定的问题
  • 失去了顺序存储结构随机存取的特点
2.8 循环链表

所谓循环链表,就是在将单链表中尾结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表即为循环链表。它解决了如何从当中一个结点出发,访问到链表的全部结点。

在这里插入图片描述

循环链表不一定要头结点,只是为了处理一致

循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断下一个元素是否为空,现在则是下一个元素不等于头结点即可。

同时,在单链表中,我们访问第一个结点需要O(1)的时间,而访问最后一个结点则需要O(n)的时间。而在循环链表,我们可以通过使用尾指针来表示循环链表,那么访问最后一个结点和第一个结点都只需要O(1)的时间了。

在这里插入图片描述

2.9 双向链表

在单链表中,查找下一个结点的时间复杂度为O(1),而如果要查找上一个结点,最坏的时间复杂度则为O(n)(即从头开始遍历查找)。为克服单向性,双向链表应运而生。双向链表是在单链表的每个结点中,再设置一个指向前驱结点的指针域

在这里插入图片描述

//线性表的双向链表存储结构
typedef struct DulNode{
    ElemType data;
    struct DulNode *prior; //直接前驱指针
    struct DulNode *next;  //直接后继指针
} DulNode, *DuLinkList;

当然,双向链表也可以是实现循环链表,这里不再描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值