数据结构——第二章《线性表》

第二章线性表

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

1,定义(逻辑上是线性的)

线性表是具有相同数据类型的n(n20)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。

1,值得注意的特性

数据元素同类型、有限、有序

2,重要术语

a;是线性表中的“第i个”元素线性表中的位序
a1是表头元素;a是表尾元素。

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

2,基本操作
1,创销、增删改查(所有数据结构适用的)

InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。

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

LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。

2,其他常用操作:

Length(L):求表长。返回线性表L的长度,即L中数据元素的个数
PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
Empty(L):判空操作。若L为空表,则返回true,否则返回false。

2.2线性表的顺序表示

2.2.1顺序表的定义

顺序表——用顺序存储的方式实现线性表顺序存储。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

1,存储结构

顺序存储——逻辑上相邻的数据元素物理上也相邻。

2,实现方式
1,静态分配

使用“静态数组”实现
大小一旦确定就无法改变

2,动态分配

使用“动态数组”实现。
L.data = (ElemType *) malloc (sizeof(ElemType) * size);
顺序表存满时,可再用malloc动态拓展顺序表的最大容量.
需要将数据元素复制到新的存储区域,并用free函数释放原区域.

3,特点

①随机访:问能在O(1)时间内找到第i个元素

②存储密度高,每个节点只存储数据元素

③拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)

④插入、删除操作不方便,需要移动大量元素

2.2.2顺序表的插入删除
1,插入
1,代码实现

Listinsert(&L,i,e)
将元素e插入到L的第i个位置。

插入位置之后的元素都要后移

2,时间复杂度分析

最好O(1)、最坏O(n)、平均O(n)

2,删除
1,实现

ListDelete(&L,i,&e)。

将L的第i个元素删除,并用e返回。

删除位置之后的元素都要前移。

2,时间复杂度

最好O(1)、最坏O(n)、平均O(n)

3,代码要点

代码中注意位序i和数组下标的区别
算法要有健壮性,注意判断i的合法性

跨考同学注意:移动元素时,从靠前的元素开始?还是从表尾元素开始?
分析代码,理解为什么有的参数需要加“&”引用

如果不加“&”,则被调用函数中处理的是参数数据的复制品

2.2.3顺序表的查找
1,按位查找
1,代码实现

GetElem(L,i)

获取表L中第i个位置的元素的值
用数组下标即可得到第i个元素L.data[i-1]。

2,时间复杂度

O(1)

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

2,按值查找
1,代码实现

LocateElem(L,e)

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

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

2,时间复杂度

最好O(1):目标元素在第一个位置
最坏O(n):目标元素在最后一个位置
平均O(n):目标元素在每个位置的概率相同

2.3线性表的链式表示

2.3.1单链表的定义
1,概念

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

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

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

优点:不要求大片连续空间,改变容量方便
缺点:不可随机存取,要耗费一定空间存放指针

2,用代码定义一个单链表
typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode, *LinkList;
3,两种实现
1,不带头结点

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

2,带头结点

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

4,补充

typedef关键字的用法

"LinkList"等价于"LNode*"前者强调这是链表,后者强调这是结点合适的地方使用合适的名字,代码可读性更高

2.3.2单链表的插入删除
1,插入
1,按位序插入

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

//在第i个位置插入元素e,带头节点
bool ListInsert(LinkList &L, int i, ElemType e)
{
    if(i<1)
        return false;
    LNode *p; //指针P指向当前扫描到的节点
    int j = 0;//当前P指向的是第几个节点
    p = L;	//L指向头节点,头节点是第0个节点,不存储数据 
    while(p!=NULL && j<i-1)//循环找到第i-1个节点
    {
        p=p->next;
        j++;
    }
    if(p==NULL)
        return false;//i值不合法
    LNode *s = (LNode *)malloc(sizeof(Lnode));
   s->data=e;
   s->next==p->next;
   p->next=s;
   return true;
}
//在第i个位置插入元素e,不带头节点
//不带头结点当i=1需要特殊处理
bool ListInsert(LinkList &L, int i, ElemType e)
{
    if(i<1)
        return false;
    if(i==1)
    {
    	LNode *s = (LNode *)malloc(sizeof(LNode));
    	s->data=e;
    	s->next=L;
    	L=S;	//头指针指向新节点
    	return true;
    }
    LNode *p; //指针P指向当前扫描到的节点
    int j = 1;//当前P指向的是第几个节点,在带头节点时这里是0.
    p = L;	//L指向头节点,头节点是第0个节点,不存储数据 
    while(p!=NULL && j<i-1)//循环找到第i-1个节点
    {
        p=p->next;
        j++;
    }
    if(p==NULL)
        return false;//i值不合法
    LNode *s = (LNode *)malloc(sizeof(Lnode));
   s->data=e;
   s->next==p->next;
   p->next=s;
   return true;
}
2,指定节点的后插操作
//后插操作:在p节点后插入元素e
bool InsertNextNode(LNode *p, ElemType e)
{
    if(p == NULL)
    	return false;
    LNode *s = (LNode *)malloc(sizeof(LNode));
    if(s == NULL)//内存分配失败
    {
        return false;
    }
    s->data = e;
    p->next = s;
    s->next = NULL;
    return true;
}
3,指定节点的前插操作
//前插操作:在p节点前面插入元素e
bool InsertBeforeNode(LNode *p, ElemType e)
{
    if(p == NULL)
        return false;
    LNode *s = (LNode *)malloc(sizeof(LNode));
    if(s == NULL)
        return false;
    s->next = p->next;
    p->next = s;	//新节点s连接到p后面
    s->data = p->data;//将p中的元素复制到s中
    p->data = e;	//将p的元素用e覆盖
    return true;
}
2,删除
1,按位序删除

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

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

bool ListDelete(LinkList &L,int i, ElemType &e)
{
    if(i < 1)	
        return false;
    LNode *p;
    int j = 0;
    while(p!=NULL && j < i - 1)
    {
        p++;
        j++;
    }
    if(p==NULL)	//i值不合法,过大
        return false;
    if(p->next == NULL)//第i-1个节点后没有节点,无法删除,i值不合法
        return false;
    LNode *d = p->next; //要删除的节点
    p->next = d->next;//将要删除的节点从链表中断开
    e = d->data;//用e返回要删除的元素的值
    free(d);
}
2,指定节点删除

删除结点p,需要修改其前驱结点的next 指针

方法1:传入头指针,循环寻找p的前驱结点
方法2:偷天换日(类似于结点前插的实现)

bool DeleteNode(LNode *p)
{
    if(p == NULL)
        return false;
    LNode *s = p->next;	//将节点s指向要删除节点p的后继节点
    p->data = s->data;//将节点p和其后继节点交换数据
    p->next = s->next;//将节点p指向其后继节点的下一个节点
    free(s);//释放p的后继节点
}

有坑:指定结点是最后一个结点时,需要特殊处理

2.3.3单链表的查找
1,按位查找

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

LNode *GetElem(LinkList L, int i)
{
    if(i < 0)
        return NULL;
    LNode *p = L;//指向当前扫描到的节点
    int j = 0;	//当前p指向的是第几个节点
    while(p != NULL && j < i)//循环找到第i个节点
    {
        p++;
        j++;
    }
    return p;
}
2,按值查找

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

LNode *LocateElem(LNode *L, ElemType e)
{
    LNode *p = L;//当前扫描到的节点
    while(p != NULL)
    {
        if(p->data == e)
            break;
        p= p->next;
    }
    return p;
}
3,求单链表长度
int Length(LinkList L)
{
    int len = 0;
    LNode *p = L;
    while(p != NULL)
    {
        len++;
        p = p-> next;
    }
    return len;
}
4,Key

三种基本操作的时间复杂度都是O(n)
如何写循环扫描各个结点的代码逻辑
注意边界条件的处理

2.3.4单链表的建立

step1:初始化一个单链表
Step2:每次娶一个数据元素,插入到表尾/表头

1,尾插法

对链表的尾节点执行后插操作

//定义单链表的节点类型
typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode. *LinkList;
//初始化一个带头结点的单链表
bool InitList(LinkList &L)
{
    L = (LNode *)malloc(sizeof(LNode));//分配一个头节点
    if(L == NULL)
        return false;
    L-Next = NULL;	//头节点之后暂时还没有节点	
    return true;
}
//尾插法建立单链表
LinkList List_TailInsert(LinkList &L)
{
    int x;
    L = (LNode *)malloc(sizeof(LNode));
    L = NULL;
    LNode *s;
    LNode *r=L;		//r为表尾指针
    scanf("%d", &x);
    while(x!=999)//结束条件
    {
        s = (LNode *)malloc(sizeof(LNode));	//为新节点分配空间
        s->data = x;//给新节点赋值
        r->next = s;//把新节点插入到链表尾上,后插
        r = s;	//把r始终指向表尾节点
        scanf("%d", &x);
    }
   r->next = NULL;	//尾结点指针置空
   return L;
}
2,头插法

对链表的头节点执行后插操作。

LinkList List_HeadInsert(LinkList &L)
{
    L = (LNode *)malloc(sizeof(LNode));
    L = NULL;
    LNode *r = L;
    int x;
    scnaf("%d", &x);
    while(x != 999)
    {
        r = (LNode *)malloc(sizeof(Lnode));
        r->data = x;
        r->next = L->next;
        L->next = r;
        scanf("%d", &x);
    }
    r->next = NULL;
    return L;
}
2.3.5双链表
1,初始化(带头节点)

头结点的prior、next都指向NULL

//双链表的定义
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->next = NULL;
    L->prior = NULL;
    return true;
}
2,插入(后插)

注意新插入结点、前驱结点、后继结点的指针修改边界情况;

新插入结点在最后一个位置,需特殊处理

//在p节点之后插入节点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;
    p->next =s;
    s->prior = p;
}
3,删除(后删)

注意删除结点的前驱结点、后继结点的指针修改边界情况;

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

//删除P节点的后继节点
bool DeleteNextDNode(DNode *p)
{
    if(p == NULL)
        return false;
    DNode *q = p->next;//p的后继节点
    if(q == NULL)
        return false;
    p->next = q->next;
    if(q->next != NULL)
        q->next->prior = p;
    free(q);
}
//删除整个链表
void DestoryList(DLinklist &L)
{
    while(L->next != NULL)
    {
        DeleteNextDnode(p);
    }
    free(L);
    L = NULL;
}
4,遍历

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

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

2.3.6循环链表
1,循环单链表

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

//初始化一个循环单链表
bool InitList(LinkList &L)
{
    L = (LNode *)malloc(sizeof(LNode));
    if(L == NULL)
        return false;
    L->next = L;//头结点的next指向头结点自身
    return true;
}
//判断循环单链表是否为空
bool Empty(LinkList L)
{
    if(L->next == L)
        return true;
    else
        retrun false;
}
//判断节点p是否为循环单链表的尾节点
bool isTail(LinkList L, LNode *p)
{
    if(p->next == L)
        return true;
    else 
        return false;
}
2,循环双链表

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

表尾结点的 next指向头结点

//定义双链表
typedef struct DLnode{
	ElemType data;
	struct DLnode *next, *prior;
}DLnode, *DLinkList;
//初始化空的循环双链表
bool InitDLinkList(DLinkList &L)
{
    L = (DLnode *)malloc(sizeof(DLnode));
    if(L == NULL)
        return false;
    L->next = L;
    L->prior = L;
    return true;
}
//判断循环双链表是否为空
bool Empty(DLinkList L)
{
    if(L->next == L)
        return true;
    else
        return false;
}
//判断节点p是否为循环双链表的表尾节点
bool IsTail(DLinkList L, DLnode *p)
{
    if(p->next == L)
        return true;
    else
        return false;
}
//在循环双链表的p节点后插入节点s
bool InsertNextDLinkList(DLnode *p, DLnode *s)
{
    s->next=p->next;
    s->prior = p;
    p->next->prior = s;
    p->next = s;
}
3,代码问题

1,如何判空

2,如何判断结点p是否是表尾/表头结点(后向/前向遍历的实现核心)

3,如何在表头、表中、表尾插入/删除一个结点(插入、删除操作的不易错思路)

2.3.7静态链表表
1,什么是静态链表

静态链表:分配一整片连续的内存空间,各个结点集中安置。用数组的方式实现的链表

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

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

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

2,如何定义静态链表
#define MaxSize 10//静态链表的最大长度
struct Node{
    ElemType data;
    int next;//下一个元素的数组下标
};
3,简述基本操作的实现

初始化静态链表:把a[0]的next设为-1

把其他结点的next 设为一个特殊值用来表示结点空闲,如-2。

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

插入位序为i的结点:

①找到一个空的结点,存入数据元素

②从头结点出发找到位序为i-1的结点

③修改新结点的 next

④修改i-1号结点的next

删除某个结点:

①从头结点出发找到前驱结点

②修改前驱结点的游标

③被删除结点next设为-2

2.3.8顺序表和链表的比较
1,逻辑结构

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

一对一

2,存储结构(物理结构)
顺序表

优点:支持随机存取、存储密度高

缺点:大片连续空间分配不方便,改变容量不方便

链表

优点::离散的小空间分配方便,改变容量方便

缺点:不可随机存取,存储密度低

3,数据的运算/基本操作
1,创
  • 顺序表

需要预分配大片连续空间。若分配空间过小,则之后不方便拓展容量;

若分配空间过大,则浪费内存资源;

静态分配:静态数组(容量不可改变)

动态分配:动态数组(malloc、free)容量可改变

  • 链表

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

2,销毁
  • 顺序表

修改Length = 0

静态分配:静态数组(系统自动回收空间)

动态分配:动态数组(malloc、free)需要手动free

  • 链表

依次删除各个节点,free

3,增/删
  • 顺序表

插入/删除元素要将后续元素都后移/前移

时间复杂度O(n),时间开销主要来自移动元素

若数据元素很大,则移动的时间代价很高

  • 链表

插入/删除元素只需修改指针即可

时间复杂度O(n),时间开销主要来自查找目标元素

查找元素的时间代价更低

4,查找
  • 顺序表

按位查找:0(1)按值查找:0(n)

若表内元素有序,可在O(log2n)时间内找到

  • 链表

按位查找:O(n)

按值查找:O(n)

4,选择

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值