数据结构笔记——线性表

目录

一、线性表的类型定义

二、顺序表

三、链表

1.单链表(线性链表)

2.循环链表

3.双向链表


一、线性表的类型定义

线性表是最常用且最简单的一种数据结构。一个线性表是n个数据元素的有限序列。线性表中的每个元素可以是一个数、一个符号甚至更复杂的内容,但是同一线性表中元素必定有相同的特性,即属同一数据对象,相邻数据元素之间存在着序偶关系

除此之外,线性表一定满足线性结构的特点

线性结构的特点:在数据元素的非空有限集中,(1)存在唯一的一个被称做“第一个”的数据元素;(2)存在唯一的一个被称做“最后一个”的数据元素;(3)除第一个之外,集合中的每个数据元素均只有一个前驱;(4)除最后一个之外,集合中每个数据元素均只有一个后继。

线性表有两种表示方式(存储方式):一种是顺序结构存储,我们叫做顺序表;另一种是链式结构存储,我们叫做链表。这会在后面分别介绍。

线性表的基本操作:

InitList(&L)        // 构造一个空的线性表L
DestroyList(&L)        // 销毁已存在的线性表L
ClearList(&L)        // 将L重置为空表
ListInsert(&L, i, e)        // 在线性表L中的第i个位置插入新元素e
ListDelete(&L, i, &e)        // 删除线性表L中的第i个元素,并用e返回其值
IsEmpty(L)        // 判断L是否为空表
ListLength(L)        // 返回线性表L中的数据元素个数
LocateElem(L, e)        // 查找与给定值e满足特定关系的数据元素的位序,若不存在则返回0
GetElem(L, i, &e)        // 用e返回线性表L中第i个数据元素的值

二、顺序表

顺序表是用一组地址连续的存储单元依次存储线性表的数据元素。

假设线性表的每个元素占 l 个字节,那么根据顺序表的特性,可以得到第 i 个元素的地址和第 1 个元素的地址关系为LOC(a_i) = LOC(a_1) + (i - 1) * l

顺序表的图示结构:

接下来看看顺序表的代码实现:

1.存储结构:

#define LIST_INIT_SIZE 100    // 线性表存储空间的初始分配量
#define LISTINCREMENT 10    // 线性表存储空间的分配增量
typedef struct{
	ElemType *elem;    // 存储空间基址
	int length;    // 当前长度
	int listsize;    // 当前分配的存储容量(以sizeof(ElemType)为单位)
}SqList;

2.初始化操作:

Status InitList_Sq(SqList &L){
    // 构造一个空的线性表L
    L.elem = (ElemType *)malloc(LIST_INIT_SIZE * sizeof(ElemType));    // malloc()为头文件stdlib.h下的分配地址函数
    if(!L.elem) exit(OVERFLOW);    // 存储分配失败
    L.length = 0;   // 空表长度为0
    L.listsize = LIST_INIT_SIZE;    // 初始存储容量
    return OK;
} // InitList_Sq

时间复杂度:O(1)

3.销毁操作:

void DestroyList_Sq(SqList &L){
    // 销毁线性表L
    if(L.elem) free(L.elem);    // 释放存储空间
} // DestroyList_Sq

时间复杂度:O(1)

4.清空操作: 

void ClearList_Sq(SqList &L){
    // 将线性表L重置为空表
    L.length = 0;    // 将线性表长度重置为0
} // ClearList_Sq

时间复杂度:O(1) 

5.插入操作:

Status ListInsert_Sq(SqList &L, int i, ElemType e){
    // 在顺序线性表L中第i个位置之前插入新的元素e
    // i的合法值为1<=i<=ListLength(L)+1
    if(i < 1 || i > L.length + 1) return ERROR;    // i值不合法
    if(L.length >= L.listsize){    // 当前存储空间已满,增加分配
        newbase = (ElemType *)realloc(L.elem, (L.listsize + LISTINCREMENT) * sizeof(ElemType));    // realloc()为头文件stdlib.h下的重新分配地址函数
        if(!newbase) exit(OVERFLOW);    // 存储分配失败
        L.elem = newbase;    // 新基址
        L.listsize += LISTINCREMENT;    // 增加存储容量
    }
    q = &(L.elem[i - 1]);    // q为插入位置
    for(p = &(L.elem[L.length - 1]); p >= q; --p) *(p + 1) = *p;    // 插入位置及之后的元素右移
    *q = e;    // 插入e
    ++L.length;    // 表长增1
    return OK;
} // ListInsert_Sq

平均操作次数:E_{is} = \frac{1}{n + 1}\sum_{i = 1}^{n + 1}(n - i + 1) = \frac{n}{2}

时间复杂度:O(n)

6.删除操作:

Status ListDelete_Sq(SqList &L, int i, ElemType &e){
    // 在顺序线性表L中删除第i个元素,并用e返回其值
    // i的合法值为1<=i<=ListLength(L)
    if(i < 1 || i > L.length) return ERROR;    // i值不合法
    p = &(L.elem[i - 1]);    // p为被删除元素的位置
    e = *p;    // 被删除元素的值赋给e
    q = L.elem + L.length - 1;    // 表尾元素的位置
    for(++p; p <= q; ++p) *(p - 1) = *p;    // 被删除元素之后的元素左移
    --L.length;    // 表长减1
    return OK;
} // ListDelete_Sq

平均操作次数:E_{dl} = \frac{1}{n}\sum_{i = 1}^{n}(n - i) = \frac{n - 1}{2}

时间复杂度:O(n)

7.判断空表:

int IsEmpty_Sq(SqList L){
    // 判断线性表L是否为空
    if(L.length == 0) return TRUE;    // 数据元素长度为0,返回1;反之返回0
    else return FALSE;
} // IsEmpty_Sq

时间复杂度:O(1) 

8.求长度操作:

int ListLength_Sq(SqList L){
    // 求线性表L的长度
    return L.length;
} // ListLength_Sq

时间复杂度:O(1) 

9.查找操作:

int LocateElem_Sq(SqList L, ElemType e){
    // 在顺序线性表L中查找第1个值与e满足compare()的元素的位序
    // 若找到,则返回其在L中的位序,否则返回0
    i = 1;    // i的初值为第1个元素的位序
    p = L.elem;    // p的初值为第1个元素的存储位置
    while(i <= L.length && !compare(*p++, e)) ++i;    // compare()为查找的条件
    if(i <= L.length) return i;    // 若找到则返回数据元素的位序
    else return 0;
} // LocateElem_Sq

平均操作次数:E_{loc} = \frac{1}{n}\sum_{i = 1}^{n}i = \frac{n + 1}{2}(其中n为顺序表中数据元素的个数)

时间复杂度:O(n)

10.取值操作:

Status GetElem_Sq(SqList L, int i, ElemType &e){
    // 用e返回顺序表L中第i个元素的值
    if(i < 1 || i > L.length) return ERROR;    // 判断i的值是否合理
    e = L.elem[i - 1];    // 将第i个元素值赋给e
    return OK;
} // GetElem_Sq

时间复杂度:O(1)

总结

顺序表的优点

(1)存储密度大(结点本身所占存储量/结点结构所占存储量)

(2)可以随机存取表中任意元素

顺序表的缺点

(1)再插入、删除某一元素时,需要移动大量元素

(2)浪费存储空间

(3)属于静态存储形式,数据元素的个数不能自由扩充

三、链表

链表是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。

链表中每个数据都有两部分信息,一部分是自身元素的信息(数据域),另一部分是指示前继或后继的信息(指针域),这两部分组成的数据元素的存储映像称为结点。指针域中存储的信息称为指针链。

1.单链表(线性链表)

单链表是每个结点只有一个数据域一个指针域构成,且指针域指示后继结点的信息,尾元结点的指针为空(NULL)。

单链表的图示结构:

接下来看看单链表的代码实现:

1.存储结构:

typedef struct LNode{
	ElemType data;    // 数据元素
	struct LNode *next;    // 指针元素
}LNode, *LinkList;

2.初始化操作:

Status InitList_L(LinkList &L){
    // 初始化单链表L
    L = (LinkList)malloc(sizeof(LNode));    // 生成头结点
    L->next = NULL; // 头结点指针初始化为空
    return OK;
} // InitList_L

时间复杂度:O(1)

3.销毁操作:

Status DestroyList_L(LinkList &L){
    // 销毁单链表L
    LNode *p;   // 或LinkList p;
    while(L){   // 从头结点开始依次释放结点
        p = L;
        L = L->next;
        free(p);    // 释放结点
    }
    return OK;
} // DestroyList_L

时间复杂度:O(n)​​​​​​ 

4.清空操作:

Status ClearList_L(LinkList &L){
    // 清空单链表L
    LNode *p, *q;   // 或LinkList p, q;
    p = L->next;
    while(p){    // 循环单链表,从首元结点开始释放结点
        q = p->next;
        free(p);
        p = q;
    }
    L->next = NULL;    // 头结点指针变为空
    return OK;
} // ClearList_L

时间复杂度:O(n)

5.插入操作:

Status ListInsert_L(LinkList &L, int i, ElemType e){
	// 在带头结点的单链线性表L中第i个位置之前插入元素e
	p = L; j = 0;
	while(p && j < i - 1){    // 寻找第i-1个结点
		p = p->next; ++j;
	}
	if(!p || j > i - 1) return ERROR;    // i小于1或者大于表长加1
	s = (LinkList)malloc(sizeof(LNode));    // 生成新结点
	s->data = e;    // 插入L中
	s->next = p->next;
	p->next = s;
	return OK;
} // ListInsert_L

时间复杂度:查找部分O(n),插入部分O(1)

6.删除操作:

Status ListDelete_L(LinkList &L, int i, ElemType &e){
	// 在带头结点的单链线性表L中,删除第i个元素,并由e返回其值
	p = L; j = 0;
	while(p->next && j < i - 1){    // 寻找第i个结点,并令p指向其前驱
		p = p->next; ++j;
	}
	if(!(p->next) || j > i - 1) return ERROR;    // 删除位置不合理
	q = p->next;    // 删除并释放结点
	p->next = q->next;
	e = q->data;
	free(q);
	return OK;
} // ListDelete_L

时间复杂度:查找部分O(n),删除部分O(1)

7.判断空表:

int IsEmpty_L(LinkList L){
    // 判断链表L是否为空
    if(L->next) return FALSE;   // 非空
    else return TRUE;
} // IsEmpty_L

时间复杂度:O(1)

8.求长度操作:

int ListLength_L(LinkList L){
    // 求单链表L的表长
    LNode *p;
    p = L->next;    // p指向首元结点
    i = 0;
    while(p){    // 遍历单链表,统计结点数
        i++;
        p = p->next;
    }
    return i;    // 返回结点数
} // ListLength_L

时间复杂度:O(n)

9.查找操作:

LNode *LocateElem_L(LinkList L, ElemType e){
    // 在单链表L中查找满足条件compare()的数据元素
    // 若找到则返回该数据元素的地址,否则返回NULL
    p = L->next;
    // int j = 0;    若要返回位序则定义一个j记数据的位置
    while(p && !compare(p->data, e)){
        p = p-> next;
        // j++;
    }
    if(L->next != NULL) return p;    // 若要返回位序,则return j;
    else return NULL;
} // LocateElem_L

时间复杂度:O(n)

10.取值操作:

Status GetElem_L(LinkList L, int i, ElemType &e){
	// L为带头结点的单链表的头指针。
	// 当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR
	p = L -> next; j = 1;    // 初始化,p指向第1个结点,j为计数器
	while(p && j < i){    // 顺指针向后查找,直到p指向第i个元素或p为空
		p = p->next; ++j;
	}
	if(!p || j > i) return ERROR;    // 第i个元素不存在
	e = p->data;    // 取第i个元素
	return OK;
} // GetElem_L

时间复杂度: 查找部分O(n),取值部分O(1)

11.头插法创建单链表

void CreateList_H(LinkList &L, int n){
	// 逆位序输入n个元素的值,建立带表头结点的单链线性表L
	L = (LinkList)malloc(sizeof(LNode));
	L->next = NULL;    // 先建立一个带头结点的单链表
	for(i = n; i > 0; --i){
		p = (LinkList)malloc(sizeof(LNode));    // 生成新结点
		scanf(&p->data);    // 输入元素值
		p->next = L->next;    // 插入到表头
		L->next = p;
	}
} // CreateList_H

时间复杂度:创建 n 个元素的表为O(n),每次插入元素为O(1)

12.尾插法创建单链表

void CreateList_R(LinkList &L, int n){
    // 正位序输入n个元素的值,建立带表头结点的单链表L
    L = (LinkList)malloc(sizeof(LNode));
    L->next = NULL;    // 先建立一个带头结点的单链表
    r = L;
    for(i = 0; i < n; ++i){
        p = (LinkList)malloc(sizeof(LNode));    // 生成新结点
		scanf(&p->data);    // 输入元素值
		p->next = NULL;
		r->next = p;    // 插入到表尾
		r = p;    // 指向新的尾元结点
    }
} // CreateList_R

时间复杂度:创建 n 个元素的表为O(n),每次插入元素为O(1)

2.循环链表

循环链表是每个结点只有一个数据域一个指针域构成,且指针域指示后继结点的信息,尾元结点的指针域指示头结点(或首元结点)的信息。

循环链表有两种表示形式:

第一种是用头指针表示,那么表示如图所示:

这种方法查找a_1的时间复杂度为O(1),但查找a_n的时间复杂度为O(n),实用性较低;

第二种是用尾指针表示,表示如图所示:

这种方法查找不管是查找a_1还是a_n时间复杂度都是O(1),实用性较高。

循环链表的大部分操作都与单链表类似,这里就不再赘述了,可以自行进行分析。

3.双向链表

双向链表是每个结点只有一个数据域两个指针域构成,指针域指示前驱结点和后继结点的信息,头结点的前驱指针为NULL,尾元结点的后继指针为NULL。

双向链表的图示结构:

双向循环链表的图示结构:

这里重点来看双向(循环)链表存储结构和双向循环链表的插入、删除操作:

1.存储结构:

typedef struct DuLNode{
	ElemType data;    // 数据元素
	struct DuLNode *prior;    // 指向前驱的指针
	struct DuLNode *next;    // 指向后继的指针
}DuLNode, *DuLinkList;

2.插入操作:

Status ListInsert_DuL(DuLinkList &L, int i, ElemType e){
    // 在带头结点的双链循环线性表L中第i个位置之前插入元素e
    // i的合法值为1<=i<=表长+1
    if(!(p == GetElemP_DuL(L, i)))    // 在L中确定插入位置
        return ERROR;    // p = NULL,即插入位置不合法
    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

时间复杂度:O(1)

3.删除操作:

Status ListDelete_DuL(DuLinkList &L, int i, ElemType &e){
    // 删除带头结点的双链循环线性表L的第i个元素,i的合法值为1<=i<=表长
    if(!(p == GetElemP_DuL(L, i)))    // 在L中确定删除位置
        return ERROR;    // p = NULL,即插入位置不合法
    e = p->data;
    p->prior->next = p->next;
    p->next->prior = p->prior;
    free(p);
    return OK;
} // ListDelete_DuL

时间复杂度:O(1)

总结

链表的优点

(1)能灵活地分配内存空间

(2)插入或删除元素效率高,每次操作仅需O(1)的时间

链表的缺点

(1)不像数组一样通过下表读取元素,而是每次都要从表头开始一个一个读取

(2)查询第k个元素需要O(k)的时间

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值