数据结构第二章 线性表

2.1_线性表的定义和基本操作

线性表是具有相同数据类型的n(n>=0)个数据元素有限序列,其中n为表长,当n=0时线性表是一个空表。若用L命名线性表,则其一般表示为

各个数据元素所占空间一样大

ai是线性表中的第“第i个“元素线性表中的位序

a1是表头元素;an是表尾元素

除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继

注意:位序从1开始,数组下标从0开始

线性表的基本操作:

InitList(&L):初始化表。构造一个空的线性表L,分配内存空间

DestoryList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。

ListInsert(&L,i&e):插入操作。在表L中的第i个位置上插入指定元素e。

ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。

LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。

GetElem(L,i):按位查找操作。获取L中第i个位置的元素的值。

其他常用操作:

Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。

PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。

Empty(L):判空操作。若L为空表,则返回true,否则返回false。

Tips:

对数据的操作(记忆思路)——创销、增删改查

C语言函数的定义—— <返回值类型> 函数名 (<参数1类型> 参数1,<参数2类型> 参数2,......)

实际开发中,可根据实际需求定义其他的基本操作

函数名和参数的形式、命名都可改变(Key:命名要有可读性)

什么时候要传入引用&——对参数的修改结果需要“带回来“

为什么要实现对数据结构的基本操作?

  1. 团队合作编程,你定义的数据结构要让别人能够很方便的使用(封装)
  2. 将常用的操作/运算封装成函数,避免重复工作,降低出错风险

2.2.1_顺序表的定义

顺序表——用顺序存储的方式实现线性表

顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

如何知道一个数据元素大小?

C语言sizeof(ElemType)

ElemType就是你的顺序表中存放的数据元素类型

顺序表的实现——静态分配

​
#difine MaxSize 10             //定义最大长度

typedef struct{               

ElemType data[MaxSize];   //用静态的“数组“存放数据元素

int length;                //顺序表的当前长度

}SqList;                       //顺序表的类型定义(静态分配方式)

​

给各个数据元素分配连续的存储空间,大小为MaxSize*sizeof(ElemType)

顺序表的实现——动态分配

#define InitSize 10         //顺序表的初始长度

typedef struct{

ElemType *data;       //指示动态分配数组的指针

int MaxSize;          //顺序表的最大容量

int length;            //顺序表的当前长度

}SeqList;                 //顺序表的类型定义(动态分配方式)

Key:动态申请和释放内存空间

C——malloc、free函数

顺序表的特点:

  1. 随机访问,即可以在O(1)时间内找到第i个元素
  2. 存储密度高,每个节点只存储数据元素
  3. 扩展容量不方便(即便采用动态分配的方式实现,扩展长度的时间复杂度也比较高)
  4. 插入、删除操作不方便,需要移动大量元素

静态分配:

使用“静态数组”实现

大小一旦确定就无法改变

动态分配:

L.data = (ElemType *) malloc (sizeof(ElemType) * size);

顺序表存满时,可再用malloc动态扩展顺序表的最大容量

需要将数据元素复制到新的存储区域,并用free函数释放原区域

2.2.2_1_顺序表的插入删除

ListInsert(&L,i,e):插入操作:在表L中第i个位置上插入指定元素e。

注:本节代码建立在顺序表的“静态分配”实现方式上,“动态分配”也雷同。

最好情况:新元素插入到表尾,不需要移动元素,i=n+1,循环0次;最好时间复杂度=O(1)

最坏情况:新元素插入到表头,需要将原有的n个元素全部向后移动,i=1,循环n次;最坏时间复杂度=O(n);

平均情况:假设新元素插入到任何一个位置的概率相同,即i=1,2,3,…,length+1的概率都是p=1/(n+1),i=1,循环n次;i=2,循环n-1次;… i=n+1,循环0次,平均循环次数=2/n;平均时间复杂度=O(n)

ListDelete(&L,I,&e):删除操作:删除表L 中第i个位置的元素,并用e返回删除元素的值。

最好情况:删除表尾元素,不需要移动其他元素,i=n,循环0次,最好时间复杂度=O(1)

最坏情况:删除表头元素,需要将后续的n-1个元素全都向前移动,i=1,循环n-1次;最坏时间复杂度=O(n);

平均情况:平均时间复杂度=O(n)

代码要点:

代码中注意位序i和数组下标的区别

算法要有健壮性,注意判断i的合法性

分析代码,理解为什么有的参数需要加“&”引用

2.2.2_2_顺序表的查找

顺序表的按位查找

GetElem(L,i):按位查找操作:获取表L中第i个位置的元素的值

获取表L中第i个位置的元素的值

用数组下标即可得到第i个元素L.data[i-1]

ElemType GetElem(SqList L, int i){

Return L.data[i-1];

}

时间复杂度:O(1),最好/最坏/平均时间复杂度都是O(1)

由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第i个元素——“随机存取”特性

顺序表的按值查找

LocateElem(L,e):按值查找操作:在表L中查找具有给定关键字值的元素。

在顺序表L中第一个元素值等于e的元素,并返回其位序

从第一个元素开始依次往后检索

注意:C语言中,结构体的比较不能直接用“==”

需要依次对比各个分量来判断两个结构体是否相等

最好情况:目标元素在表头,循环一次;最好时间复杂度=O(1)

最坏情况:目标元素在表尾,循环n次;最坏时间复杂度=O(n)

平均情况:假设目标元素出现在任何一个位置的概率相同,都是1/n。平均时间复杂度=O(n)

2.3.1_单链表的定义

每个结点除了存放数据元素外,还要存储指向下一个节点的指针

优点:不要求大片连续空间,改变容量方便

缺点:不可随机存取,要耗费一定空间存放指针

struct LNode{                //定义单链表结点类型

ElemType data;           //每个节点存放一个数据元素

struct LNode *next;       //指针指向下一节点

};

struct LNode * p = (struct LNode *) malloc(sizeof(struct LNode));

增加一个新的结点:再内存中申请一个结点所需空间,并用指针p指向这个结点

typedef关键字——数据类型重命名

typedef <数据类型> <别名>

typedef int zhengshu;

typedef int *zhengshuzhizhen;

定义完后可以用这样的方式来定义:

zhengshu x = 1;

zhengshuzhizhen p;

typedef struct LNode LNode;

LNode * p = (LNode *) malloc(sizeof(LNode));

更简便的方法:

typedef struct LNode{              //定义单链表结点类型

ElemType data;               //每个结点存放一个数据元素

struct LNode *next;            //指针指向下一个节点

}LNode, *LinkList;

要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点

LNode * L;   //声明一个指向单链表第一个结点的指针

或:LinkList L; //声明一个指向单链表第一个结点的指针

强调这是一个单链表——使用LinkList

强调这是一个结点——使用LNode*

不带头结点,写代码更麻烦

对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑

对空表和非空表的处理需要用不同的代码逻辑

单链表:

用“链式存储“(存储结构),实现了”线性结构“(逻辑结构)

一个结点存储一个数据元素

各结点间的先后关系用一个指针表示

两种实现:

不带头结点

空表判断:L == NULL。写代码不方便

带头结点

空表判断:L -> next == NULL。写代码更方便

头结点不存数据,只是为了操作方便

2.3.2_1_单链表的插入和删除

LinstInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。

找到第i-1个结点,将新结点插入其后

头结点可以看作“第0个“结点

如果不带头结点,则插入、删除第1个元素时,需要更改头指针L

结论:不带头结点写代码更不方便

考试中带头、不带头都有可能考察

ListDelete(&L,i,&e):删除操作:删除表L中第i个位置的元素,并用e返回删除元素的值。

找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点

最坏、平均时间复杂度:O(n)

最好时间负责度:O(1)

指定结点的删除:

如果p是最后一个结点,只能从表头开始依次寻找p的前驱,时间复杂度O(n)

单链表的局限性:无法逆向检索,有时候不太方便

2.3.2_2_单链表的查找

GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。

LoctaeElem(L,e):按值查找操作。在表L中查找具有给定关键字值得元素。

按位查找平均时间复杂度:O(n)

按值查找平均时间复杂度:O(n)

单链表不具备“随机访问“的特性,只能依次扫描

三种基本操作(按位查找、按值查找、求单链表长度)的时间复杂度都是O(n)

注意边界条件的处理

2.3.2_3_单链表的建立

单链表的建立:

尾插法、头插法

如果给你很多个数据元素(ElemType),要把它们存到一个单链表里,如何处理

Step 1:初始化一个单链表

Step 2:每次取一个数据元素,插入到表尾/表头

设置变量length记录链表长度

While循环{

          每次取一个数据元素e;

          ListInsert(L,length+1,e)插到尾部;

          length++;

}

设置一个表尾指针

头插法建立单链表:

初始化单链表

While循环{

        每次取一个数据元素e;

         InsertNextNode(L,e);

}

只要是初始化单链表,都先把头指针指向NULL

头插法的重要应用:链表的逆置

头插法、尾插法:核心就是初始化操作、指定结点的后插操作

2.3.3_双链表

单链表:无法逆向检索,有时候不太方便

双链表:可进可退,存储密度更低一点

typedef struct DNode{                //定义双链表结点类型

      ElemType Data;                //数据域

      struct DNode *prior, *next;      //前驱和后继指针

}DNode, *DLinklist;

bool InitDLinkList(DLinklist &L){

      L=(DNode *) malloc(sizeof(DNode));   //分配一个头结点

      if (L==NULL)                       //内存不足,分配失败

          return false;

      L->prior = NULL;            //头结点的prior永远指向NULL

      L->next = NULL;            //头结点之后暂时还没有结点

      return true;

}

void testDLinkList() {

     //初始化双链表

     DLinklist L;

     InitDLinkList(L);

     //后续代码

}

双链表的插入

//在p结点之后插入s结点

bool InsertNextDNode(DNode *p, DNode *s){

   s->next=p->next;       // 将结点*s插入到结点*p之后

   p->next->prior=s;

   s->prior=p;

   p->next=s;

}

改进后(如果插入位置是最后一个结点)

bool InsertNextDNode(DNode *p, DNode *s){

   if (p==NULL || s==NULL)   //非法参数

        return false;

   s->next=p->next;

    if(p->next != NULL)  //如果p结点有后继结点

          p->next->prior=s;

    s->prior=p;

p->next=s;

return true;

}

修改指针时要注意顺序

双链表的删除:

//删除p的后继结点q

p->next=q->next;

q->next->prior=p;

free(q);

增加条件判断改进

销毁一个双链表:

void DestoryList(DLinklist &L){

   //循环释放各个数据结点

   while (L->next != NULL)

          DeleteNextDNode(L);

   free(L);       //释放头结点

   L=NULL;    //头指针指向NULL

}

双链表的遍历:

后向遍历

while(p!=NULL)

//对结点p做相应处理,如打印

p=p->next;

}

前向遍历:

while(p!=NULL)

     //对结点p做相应处理

     p=p->prior

}

双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。时间复杂度O(n)

初始化:头结点的prior、next都指向NULL

插入(后插):

注意新插入结点、前驱结点、后继结点的指针修改

边界情况:新插入结点在最后一个位置,需特殊处理

删除(后删):

注意删除结点的前驱结点、后继结点的指针修改

边界情况:如果被删除结点是最后一个数据结点,需特殊处理

遍历:

从一个给定结点开始,后向遍历、前向遍历的实现(循环的终止条件)

链表不具备随机存取特性,查找操作只能通过顺序遍历实现

2.3.4_循环链表

单单链表:表尾结点的next指针指向NULL

循环单链表:表尾结点的next指针指向头结点

//判断结点p是否为循环单链表的表尾结点

bool isTail(LinkList L, LNode *p){

   if(p->next == L)

       return true;

   else

      return false;

}

单链表:从一个结点出发只能找到后续的各个结点

循环单链表:从一个结点出发可以找到其他任何一个结点

从头结点找到尾部,时间复杂度为O(n)

当对链表的操作都是在头部或尾部时,可以让L指向表尾元素(插入、删除时可能需要修改L)

这样从尾部找到头部,时间复杂度为O(1)

双链表:

表头结点的prior指向NULL

表尾结点的next指向NULL

循环双链表:

表头结点的prior指向表尾结点

表尾结点的next指向头结点

循环双链表的初始化

L->prior = L; //头结点的prior指针指向头结点

L->next = L; //头结点的next指向头结点

//在p结点之后插入s结点

bool InsertNextDNode(DNode *p, DNode *s){

s->next=p->next;    //将结点*s插入到结点*p之后

p->next->prior=s;

s->prior=p;

p->next=s;

}

2.3.5_静态链表

单链表:各个结点在内存中星罗棋布、散落天涯。

静态链表:分配一整片连续的内存空间,各个结点集中安置。

0号结点充当“头结点“

游标充当“指针“

游标为-1表示已经到达表尾

#define MaxSize 10        //静态链表的最大长度

struct Node{              //静态链表结构类型的定义

ElemType data;       //存储数据元素

int next;             //下一个元素的数组下标

};

void testSLinkList(){

   struct Node a[MaxSize];

   //后续代码

}

初始化静态链表:

把a[0]的next设为-1

查找:

从头结点出发挨个往后遍历结点

插入位序为i的结点:

  1. 找到一个空的结点,存入数据元素
  2. 从头结点出发找到位序为i-1的结点
  3. 修改新结点的next
  4. 修改i-1号结点的next

可让空闲结点next为某个特殊值用来表示结点空闲,如-2

删除某个结点:

  1. 从头结点出发找到前驱结点
  2. 修改前驱结点的游标
  3. 被删除结点next设为-2

静态链表:用数组的方式实现的链表

优点:增、删操作不需要大量移动元素

缺点:不能随机存取,只能从头结点开始依次往后查找:容量固定不可变

适用场景:不支持指针的低级语言;数据元素数量固定不变的场景(如操作系统的文件分配表FAT)

2.3.6_顺序表和链表的比较

逻辑结构:

都属于线性表,都是线性结构

存储结构:

顺序表优点支持随机存取、存储密度高;缺点大片连续空间分配不方便,改变容量不方便。

链表优点离散的小空间分配方便,改变容量方便;缺点不可随机存取,存储密度低。

基本操作:

创:

顺序表:需要预分配大片连续空间。若分配空间过小,则之后不方便扩展容量;若分配空间过大,则浪费内存资源

链表:只需分配一个头结点(也可以不要头结点,只声明一个头指针),之后方便扩展

销:

顺序表:修改Length=0;静态分配实现系统自动回收空间,动态分配需要手动free

链表:依次删除各个结点(free)

增、删:

顺序表:插入/删除元素要将后续元素都后移/前移,时间复杂度O(n),时间开销主要来自移动元素(若数据元素很大,则移动的时间代价很高)

链表:插入/删除只需修改指针即可,时间复杂度为O(n),时间开销主要来自查找目标元素(查找元素的时间代价更低)

查:

顺序表:按位查找:O(1);按值查找:O(n)

链表:按位查找O(n);按值查找O(n)

用顺序表还是链表?

表长难以预估、经常要增加/删除元素——链表

表长可预估、查询(搜索)操作较多——顺序表

问题:请描述顺序表和链表的……

答:

实现线性表时,用顺序表还是链表好?

顺序表和链表的逻辑结构都是线性结构,都属于线性表

但是二者的存储结构不同,顺序表采用顺序存储…(特点,带来的优点缺点);链表采用链式存储…(特点、导致的优缺点)

由于采用不同的存储方式实现,因此基本操作的实现效率也不同。当初始化时…;当插入一个数据元素时…;当删除一个数据元素时…;当查找一个数据元素时…

  • 24
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值