Per aspera ad astra.
循此苦
|

Eulbo_1018

园龄:7个月粉丝:0关注:0

第二章 线性表

线性表的定义和基本操作

线性表的定义

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

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

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

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

线性表的基本操作

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(Li):按位查找操作。获取表L中第i个位置的元素的值。

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

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

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

顺序表的定义

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

顺序表的特点

  1. 随机访问,可以在O(1)时间内找到第i个元素
  2. 存储密度高,每个节点只存储数据元素
  3. 拓展容量不方便,静态不可以拓展,动态分配拓展时间复杂度高
  4. 插入,删除操作不方便,需要移动大量元素

顺序表的实现

顺序表的实现—静态分配(存储空间是静态的,无法更改)

#include<stdio.h>
#define MaxSize 10 //定义最大长度
//创建静态顺序表的结构
typedef struct{
int data[MaxSize];//用静态的数组存放数据元素
int lengeh;//顺序表的长度
}SqList;//顺序表的类型定义
//初始化静态顺序表
void InitList(SqList &L){
for(int i=0;i<MaxSize;i++){
L.data[i]=0;
}
L.lengeh=0;
}
int main(){
SqList L;
InitList(L);
return 0;
}

顺序表的实现—动态分配

//动态 顺序表
#include<stdio.h>
#include<stdlib.h>
#define InitSize 10//顺序表的初始长度
struct SqList {
int* data;//只是动态分配数组的指针
int MaxSize;//顺序表的最大容量
int lengh;//顺序表的当前长度
};
void InitList(struct SqList* L) {
L->data = (int*)malloc(sizeof(int) + InitSize);
for (int i = 0; i < L->MaxSize; i++)
{
L->data[i] = 0;
}
L->lengh = 0;
L->MaxSize = InitSize;
}
//增加动态数组的长度
void IncreaseList(struct SqList* L, int len) {
//用malloc函数申请一片连续的存储空间
int* p = L->data;
L->data = (int*)malloc(sizeof(int) * (L->MaxSize + len));
//将数据复制到新区域中
for (int i = 0; i < L->lengh; i++)
{
L->data[i] = p[i];
}
L->MaxSize = L->MaxSize + len;//顺序表最长长度增加len
}
void ListInsert(struct SqList *L,int i ,int e) {
for (int j = L->lengh; j >=i; j--)
{
L->data[j] = L->data[j - 1];
}
L->data[i - 1] = e;
L->lengh++;
}
int main() {
struct SqList L = { 0 };
InitList(&L);
printf("%d\n", L.lengh);
printf("%d\n", L.MaxSize);
for (int i = 0; i < L.MaxSize; i++)
{
(&L)->data[i] = i;
}
(&L)->lengh = L.MaxSize;
for (int i = 0; i < L.lengh; i++)
{
printf("%d ", L.data[i]);
}
printf("\n");
IncreaseList(&L, 10);
printf("%d\n", L.lengh);
printf("%d\n", L.MaxSize);
for (int i = 0; i < L.lengh; i++)
{
printf("%d ", L.data[i]);
}
ListInsert(&L, 2, 2);
printf("%d\n", L.lengh);
printf("%d\n", L.MaxSize);
for (int i = 0; i < L.lengh; i++)
{
printf("%d ", L.data[i]);
}
return 0;
}

顺序表的基本操作

顺序表元素的插入

void ListInsert(struct SqList *L,int i ,int e) {
for (int j = L->lengh; j >=i; j--)
{
L->data[j] = L->data[j - 1];
}
L->data[i - 1] = e;
L->lengh++;
}

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

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

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

顺序表元素的删除

int ListDelete(struct SqList *L,int i,int *e) {
//判断
if (i<1||i>L->lengh)//判断i的值是否有效
{
return 0;
}
else
{
*e = L->data[i - 1];//将删除的元素赋值给e
for (int j = i; j < L->lengh; j++)//将第i个位置的元素前移
{
L->data[j - 1] = L->data[j];
}
L->lengh--;//有效长度减一
return 1;
}
}

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

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

平均情况 :假设删除任何一个元素的概率相同,即i=1,2,3,...length的概率都是p=1/n,则平均循环次数=(n-1)p+(n-2)p+....+p=(n-1)/2,平均时间复杂度=O(n)

顺序表的按位查找

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

ElemType GetElem(SeqList L,int i){
return L.data[i - 1];
}

顺序表的按值查找

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

ElemType LocateElem(SeqList L, ElemType e) {
for (int i = 0; i <= L.length; i++) {
if (L.data[i] == e) {
return i + 1;
}
}
return -1;
}

在注重考察语法的时候,基本数据类型可以用“==”比较

时间复杂度O(n)

单链表的定义

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

头结点和头指针的关系: 不管带不带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表的第一个结点,结点内通常不存储信息。

引入头结点后,可以带来两个优点:

  1. 由于第一个数据节点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作在表的其他位置上的操作一致,无须进行特殊处理
  2. 无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和费控表的处理也就得到了统一

单链表的特点

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

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

单链表的实现

点击查看代码
#include<stdio.h>
#include<stdlib.h>
typedef int ElemType;
typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode, *LinkList;
bool InitList(LinkList &L) {
L = NULL;
return true;
}
bool InitList_h(LinkList &L) {
L = (LNode*)malloc(sizeof(LNode));
if (L == NULL) {
return false;
}
L->next = NULL;
return true;
}
bool ListInsert_h(LinkList &L, int i, ElemType e) {
if (i < 1) {
return false;
}
LNode *p;
int j = 0;
p = L;
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
if (p == NULL) {
return false;
}
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
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;
int j = 1;
p = L;
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
if (p == NULL) {
return false;
}
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
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;
s->next = p->next;
p->next = s;
return true;
}
bool InsertPriorNode(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->data = p->data;
p->data = e;
return true;
}
//按位序删除(带头节点)
bool ListDelete(LinkList &L, int i, ElemType &e) {
if (i < 1) {
return false;
}
LNode *p;
int j = 0;
p = L;
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
if (p == NULL) {
return false;
}
if (p->next == NULL) {
return false;
}
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
bool DeleteNode(LNode *p) {
if (p == NULL) {
return false;
}
LNode *q = p->next;
p->data = p->next->data;
p->next = q->next;
free(q);
return true;
}
LNode* GetElem_h(LinkList L, int i) {
if (i < 0) {
return NULL;
}
LNode *p;
int j = 0;
p = L;
while (p != NULL && j < i) {
p = p->next;
j++;
}
return p;
}
LNode* LocateElem(LinkList L, ElemType e) {
LNode *p = L->next; //指向头结点的下一个节点
while (p != NULL && p->data != e) {
p = p->next;
}
return p;
}
int Length_h(LinkList L) {
int len = 0;
LNode *p = L;
while (p->next != NULL) {
p = p->next;
len++;
}
return len;
}
LinkList List_HeadInsert(LinkList &L) {
LNode *s;
int x;
L = (LNode*)malloc(sizeof(LNode));
L->next = NULL;
scanf("%d", &x);
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d", &x);
}
}
LinkList List_TailInsert(LinkList &L) {
int x;
L = (LNode*)malloc(sizeof(LNode));
LNode *s, *r = L;
scanf("%d", &x);
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s;
scanf("%d", &x);
}
r->next = NULL;
return L;
}
int main() {
LinkList L;
return 0;
}

结构体创建

typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode, *LinkList;typedef

初始化

带头结点

bool InitList_h(LinkList &L) {
L = (LNode*)malloc(sizeof(LNode));
if (L == NULL) {
return false;
}
L->next = NULL;
return true;
}

带头结点的单链表初始化时,需要创建一个头结点,并让头指针指向头结点。头结点的next域初始化为NULL

不带头结点

bool InitList(LinkList &L) {
L = NULL;
return true;
}

不带头结点的单链表初始化时,只需将头指针L初始化为NULL

单链表的基本操作

按位序插入(带头节点)

bool ListInsert_h(LinkList &L, int i, ElemType e) {
if (i < 1) {
return false;
}
LNode *p;
int j = 0;
p = L;
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
if (p == NULL) {
return false;
}
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}

本算法主要的时间开销在于查找第i-1个元素,,时间复杂度为O(n)

按位序插入(不带头结点)

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;
int j = 1;
p = L;
while (p != NULL && j < i - 1) {
p = p->next;
j++;
}
if (p == NULL) {
return false;
}
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}

当链表不带头结点时,需要判断插入位置i是否为1,若是,则要做特殊处理,将头指针L指向新的首结点。当链表带头结点时,插入位置i为1时不做特殊处理。

指定结点的后插操作

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;
s->next = p->next;
p->next = s;
return true;
}

在指定结点后插入新结点,时间复杂度为O(1)

指定结点的前插操作

bool InsertPriorNode(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->data = p->data;
p->data = e; //结点放后面,交换值
return true;
}

在指定结点后插入新结点,时间复杂度为O(1)

按位序删除(带头节点)

bool ListDelete(LinkList &L,int i,ElemType &e){
if(i<1){
return false;
}
LNode *p;
int j=0;
p=L;
while(p!=NULL&&j<i-1){
p=p->next;
j++;
}
if(p==NULL){
return false;
}
if(p->next==NULL){//确定第i个节点存在
return false;
}
LNode *q=p->next;
e=q->data;
p->next=q->next;
free(q);
return true;
}

时间复杂度为O(n)

指定结点的删除

bool DeleteNode(LNode *p){
if(p==NULL){
return false;
}
LNode *q=p->next;
p->data=p->next->data;
p->next=q->next;
free(q);
return true;
}

实质就是将其后继的值赋予其自身,然后再删除后继,也能使得时间复杂度为O(1).

这种算法存在缺陷,当要删除的结点是最后一个结点的时候,无法顺利删除,因为无法找到尾结点的后继

按位查找

LNode* GetElem_h(LinkList L, int i) {
if (i < 0) {
return NULL;
}
LNode *p;
int j = 0; //头结点是第0个结点
p = L;
while (p != NULL && j < i) {
p = p->next;
j++;
}
return p;
}

时间复杂度O(n)

按值查找

LNode* LocateElem(LinkList L,ElemType e){
LNode *p=L->next;//指向头结点的下一个节点
while(p!=NULL&&p->date!=e){
p=p->next;
}
return p;
}

时间复杂度O(n)

求表的长度

int Length_h(LinkList L) {
int len = 0;
LNode *p = L;
while (p->next != NULL) {
p = p->next;
len++;
}
return len;
}

带头结点的单链表中求表长len的初始长度为0,因为头结点是第0个结点。不带头结点的单链表中求表长len的初始长度为1,因为直接从第一个结点开始计数。

求表长操作的时间复杂度为O(n)

单链表的建立

尾插法

该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r,使其始终指向当前链表的尾结点

LinkList List_TailInsert(LinkList &L){
int x;
L=(LNode*)malloc(sizeof(LNode));
LNode *s,*r=L;
scanf("%d",&x);
while(x!9999){
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s;
scanf("%d",&x);
}
r->next=NULL;
return L;
}

头插法

该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后

LinkList List_TailInsert(LinkList &L){
int x;
L=(LNode*)malloc(sizeof(LNode));
LNode *s,*r=L;
scanf("%d",&x);
while(x!=9999){
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s;
scanf("%d",&x);
}
r->next=NULL;
return L;
}

采用头插法建立单链表时,读入数据的顺序与生成的链表中元素的顺序是相反的,可用来实现链表的逆置

双链表

双链表结点中有两个指priornext,分别指向其直接前驱和直接后继

双链表的特点

双链表在单链表结点中增加了一个指向前驱的指针prior,因此双链表的按值查找和按位查找的操作与单链表相同。但双链表在插入和删除操作的实现上,与单链表有着较大的不同,这是因为“链”变化时也需要对指针prior做出修改,其关键是保证在修改的过程中不断链

双链表的实现

结构体创建

typedef struct DNode {
ElemType data;
struct DNode *prior, *next;
} DNode, *DLinkList;

初始化

带头结点

bool InitLinkList_h(DLinkList &L){
L=(DNode*)malloc(sizeof(DNode));
if(L==NULL){
return false;
}
L->prior=NULL;
L->next=NULL;
}

不带头结点

bool IinitLinkList(DLinkList &L){
L=NULL;
return true;
}

双链表的基本操作

给定结点的后插操作

bool InsertNextDNode(DNode *p, DNode *s) {
if (p == NULL || s == NULL) {
return false;
}
s->next = p->next;
if (p->next != NULL) {
p->next->prior = s;
}
s->prior = p;
p->next = s;
return true;
}

给定结点的前插操作

bool InsertPriorDNode(DNode *p, DNode *s) {
if (p == NULL || s == NULL) {
return false;
}
s->next = p;
s->prior = p->prior;
if (p->prior != NULL) {
p->prior->next = s;
}
p->prior = s;
return true;
}

删除给定结点的后继结点

bool DeleteNextDNode(DNode *p) {
if (p == NULL) {
return false;
}
DNode *q = p->next;
if (q == NULL) {
return false;
}
p->next = q->next;
if (q->next != NULL) {
q->next->prior = p;
}
free(q);
return true;
}

双链表的销毁

void DestroyList(DLinkList &L) {
while (L->next != NULL) {
DeleteNextDNode(L);
}
free(L);
L = NULL;
}

循环链表

循环单链表

在循环单链表中,表尾结点*r的next指针域指向L,故表中没有指针域为NULL的结点,因此,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针L

初始化

bool InitList(LinkList &L) {
L = (LNode*)malloc(sizeof(LNode));
if (L == NULL) {
return false;
}
L->next = L;//头结点的next指向头结点
return true;
}

判断给定结点是否为循环单链表的表尾结点

bool isTail(LinkList L, LNode *p) {
if (p->next == L) {
return true;
} else {
return false;
}
}

循环双链表

在循环双链表中,头结点的prior指针还要指向表尾结点,当某结点*p为尾结点时,p->next==L;当循环双链表为空时,其头结点的prior域和next域都等于L

初始化

bool InitDLinkList(DLinkList &L) {
L = (DNode*)malloc(sizeof(DNode));
if (L == NULL) {
return false;
}
L->next = L;
L->prior = L;
return true;
}

静态链表

静态链表是用数组来描述线性表的链式存储结构,结点也有数据域和指针域,这里的指针是结点在数组中的相对位置,又称游标,指针指向下一个结点的位置。
静态链表也要预先分配一块连续的内存空间。
image

typedef struct {
ElemType data;
int next;
}SLinkList[MaxSize];//一个长度为MaxSize的数组

插入位序为i的结点

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

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

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

顺序表和链表的比较

逻辑结构

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

存储结构

顺序表

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

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

链表

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

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

基本操作

顺序表

创建:需要预分配大片连续空间,若分配空间过小,则之后不方便拓展容量;若分配空间过大,则浪费内存空间

插入、删除:要将后续元素都后移或前移,时间复杂度为O(n),主要开销是移动元素,若移动元素过大,则移动的时间代价很高

查找:按位查找:O(1) 按值查找O(n) 若表中元素有序,可在O(ln(n))时间内找到

链表

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

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

查找:按位查找O(n) 按值查找O(n)

表长难以估计,经常需要增加、删除元素 ——链表

表长可预估,查询操作较多 ——顺序表

posted @   Eulbo_1018  阅读(21)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起