(C++)数据结构课程笔记2/9 - 线性表

185 阅读11分钟

§2 - 线性表

1 - 线性表的基本概念

线性表的逻辑结构

  1. 线性表(List)是 n 个数据元素 a1, a2, …, an 的有限序列(数据元素可以是很复杂的信息)
  2. 序偶关系、前驱、后继、表长、空表、位序

抽象数据类型线性表的定义

ADT List {
	数据对象:
		D={a_i|a_i∈ElemSet,i=1,2,...,n}
	数据关系:
		R={<a_i-1,a_i>|a_i-1,a_i∈D,i=2,...,n}
	基本操作:
		初始化操作
		销毁操作
		引用型操作
		加工型操作
} ADT List

初始化操作: InitList(&L) 结果:构造空表 L

销毁操作: DestroyList(&L) 结果:销毁表 L

引用型操作: 判空:ListEmpty(L) 结果:空表返回 TRUE,否则返回 FALSE

求表长:ListLength(L) 结果:返回表长

求前驱:PriorElem(L, cur_e, &pre_e) 结果:若 cur_e 是 L 的数据元素且不是第一个,则用 pre_e 返回它的前驱;否则操作失败,pre_e 无定义

求后继:NextElem(L, cur_e, &next_e) 结果:若 cur_e 是 L 的数据元素且不是最后一个,则用 next_e 返回它的后继;否则操作失败,next_e 无定义

根据位序求元素:GetElem(L, i, &e) 结果:若 1 ≤ i ≤ ListLength(L),则用 e 返回第 i 个元素的值

根据元素求位序:LocateElem(L, e, compare()) 结果:返回 L 中第 1 个与 e 满足关系 compare() 的数据元素的位序;若这样的数据元素不存在,则返回值为0

加工型操作: 置空:ClearList(&L) 结果:置空

改变数据元素:PutElem(&L, i, e) 结果:若 1 ≤ i ≤ ListLength(L),则 L 中第 i 个元素赋值 e

插入数据元素:InsertElem(&L, i, e) 结果:若 1 ≤ i ≤ ListLength(L)+1,则 L 中第 i 个元素之前插入 e;L 的长度增 1

删除数据元素:DeleteElem(&L, i, &e) 结果:若 1 ≤ i ≤ ListLength(L),则删除 L 中第 i 个元素,并用 e 返回其值;L 的长度减 1

2 - 线性表的顺序表示和实现(顺序表)

表中相邻的两个元素其物理存储位置也相邻,即以元素在计算机内物理位置上的相邻来表示线性表中数据元素之间相邻的逻辑关系 → 下标索引,无需遍历

函数返回值表征操作是否成功,非必要

// ----------Sqlist.h----------
#ifndef SQLIST_H_
#define SQLIST_H_

typedef int T; // 数据元素类型

typedef struct {
    T*  elem;     // 存储空间基址
    int length;   // 当前长度
    int listsize; // 当前分配的存储容量
} SqList;

/*
 * 创建顺序表
 */
SqList CreateList_Sq();

/*
 * 初始化顺序表
 */
int InitList_Sq(SqList &L);

/*
 * 顺序表扩容
 */
int ExtendList_Sq(SqList &L);

/*
 * 增(L中第i个元素之前插入e)
 */
int InsertElem_Sq(SqList &L,int i,T e);

/*
 * 删(根据下标删除,并将已删除元素值转给e)
 */
int DeleteElem_Sq(SqList &L,int i,T &e);

/*
 * 查(查元素直接下标索引,这里是查位序)
 */
int LocateElem_Sq(SqList L,T e);

/*
 * 销毁顺序表
 */
void DestroyList_Sq(SqList &L);

#endif
// ----------Sqlist.cpp----------
#include "Sqlist.h"
#include <stdio.h>
#include <stdlib.h>
#define LIST_INIT_SIZE 80 // 顺序表存储空间的初始分配量
#define LIST_INCREMENT 10 // 顺序表存储空间的分配增量

/*
 * 创建顺序表
 */
SqList CreateList_Sq() {
    SqList* list=(SqList*)malloc(sizeof(SqList));
    return *list;
}

/*
 * 初始化顺序表
 * 返回1 表示初始化成功
 * 返回0 表示初始化失败
 */
int InitList_Sq(SqList &L) { // 只有在C++中才会有引用的存在
    L.elem=(T*)malloc(sizeof(T)*LIST_INIT_SIZE);
    if (!L.elem)
        return 0; // 内存不够,分配失败
    L.length=0;
    L.listsize=LIST_INIT_SIZE;
    return 1;
}

/*
 * 顺序表扩容
 * 返回1 表示扩容成功
 * 返回0 表示扩容失败
 */
int ExtendList_Sq(SqList &L) {
    T* newbase=(T*)realloc(L.elem,sizeof(T)*(L.listsize+LIST_INCREMENT));
    if (!newbase)
        return 0;
    L.elem=newbase;
    L.listsize+=LIST_INCREMENT;
    return 1;
}

/*
 * 增(L中第i个元素之前插入e)
 * 返回1 表示插入成功
 * 返回0 表示插入失败
 */
int InsertElem_Sq(SqList &L,int i,T e) {
    if (i<1||i>L.length+1) // robust
        return 0;
    
    if (L.length>=L.listsize) {
        if (!ExtendList_Sq(L))
            return 0;
    }
    
    for (int p=L.length+1;p>i;p--)
        L.elem[p]=L.elem[p-1];
    L.elem[i]=e;
    L.length++;
    return 1;
}

/*
 * 删(根据下标删除,并将已删除元素值转给e)
 * 返回1 表示删除成功
 * 返回0 表示删除失败
 */
int DeleteElem_Sq(SqList &L,int i,T &e) {
    if (i<1||i>L.length) // robust
        return 0;
    e=L.elem[i];
    for (int p=i;p<L.length;p++) {
        L.elem[p]=L.elem[p+1];
    }
    L.length--;
    return 1;
}

/*
 * 查(查元素直接下标索引,这里是查位序)
 * 若存在,则返回它的位序,否则返回0
 */
int LocateElem_Sq(SqList L,T e) {
    int i;
    for (i=1;i<=L.length;i++) {
        if (...) // 满足条件
            break;
    }
    if (i==L.length+1)
        return 0;
    else
        return i;
}

/*
 * 销毁顺序表
 */
void DestroyList_Sq(SqList &L) {
    free(L.elem);
	free(L);
}

*realloc:

  1. 如果当前连续内存块足够 realloc 的话,只是将 p 所指向的空间扩大,并返回 p 的指针地址,这个时候 q 和 p 指向的地址是一样的
  2. 如果当前连续内存块不够长度,再找一个足够长的地方,分配一块新的内存,并将 p 指向的内容复制到 q,返回 q,并将 p 所指向的内存空间删除

顺序表的优缺点

  1. 存储密度高(无需为表示数据元素之间的关系而增加额外存储空间)
  2. 下标索引
  3. 插入和删除运算时,必须移动大量元素,效率较低
  4. 必须预先为线性表分配连续空间(难以准确估计线性表最大长度,估计过小导致溢出,估计过大又会造成存储空间浪费)

3 - 线性表的链式表示和实现(链表)

单链表

为了表示每个元素 a_i 与其后继 a_i+1 之间的逻辑关系,一个结点包括两部分:数据域 data 存放数据元素 a_i,指针域 next 存放指向后继元素 a_i+1 所在结点的一个指针

单链表可由头指针唯一确定(一般有头结点,头结点数据域可存放 length)

注意画图

// ----------Linklist.h----------
#ifndef LINKLIST_H_
#define LINKLIST_H_

typedef int T; // 数据元素类型

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

extern LinkList head;

int InitList_L(LinkList &head);
int InsertElem_L(LinkList &head,int i,T e);
int DeleteElem_L(LinkList &head,int i,T &e);
void ClearList_L(LinkList &head);
int GetElem_L(LinkList &head,int i,T &e);
int LocateElem_L(LinkList head,T e);
void CreateList_H(LinkList &head,int n);
void CreateList_E(LinkList &head,int n);
void DestroyList_L(LinkList &head);

#endif
// ----------Linklist.cpp----------
#include "Linklist.h"
#include <stdio.h>
#include <stdlib.h>

/*
 * 创建链表
 */
LinkList head=NULL; // 含头结点

/*
 * 初始化链表
 * 返回1 表示初始化成功
 * 返回0 表示初始化失败
 */
int InitList_L(LinkList &head) {
    LinkList tmp=(LinkList)malloc(sizeof(LNode));
    if (!tmp)
        return 0;
    tmp->next=NULL;
    head=tmp;
    return 1;
}

/*
 * 增(L中第i个元素之前插入e)
 * 返回1 表示插入成功
 * 返回0 表示插入失败
 */
int InsertElem_L(LinkList &head,int i,T e) {
    LinkList p=head; int j=1;
    while (p&&j<i) {
        p=p->next; j++; // 画图理解:p指向第i-1个元素
    }
    if (!p||j>i) // robust,对应i>length+1和i<1两种情况
        return 0;
    LinkList s=(LinkList)malloc(sizeof(LNode));
    if (!s)
        return 0;
    s->data=e;
    s->next=p->next;
    p->next=s;
    return 1;
}

/*
 * 删(根据下标删除,并将已删除元素值转给e)
 * 返回1 表示删除成功
 * 返回0 表示删除失败
 */
int DeleteElem_L(LinkList &head,int i,T &e) {
    LinkList p=head; int j=1;
    while (p->next&&j<i) {
        p=p->next; j++;
    }
    if (!(p->next)||j>i) // robust,对应i>length和i<1两种情况
        return 0;
    LinkList s=p->next;
    p->next=s->next;
    e=s->data;
    free(s);
    return 1;
}

/*
 * 清空链表
 */
void ClearList_L(LinkList &head) {
    while (head->next) {
        LinkList s=head->next;
        head->next=s->next;
        free(s);
    }
}

/*
 * 取第i个数据元素
 * 返回1 表示取值成功
 * 返回0 表示取值失败
 */
int GetElem_L(LinkList &head,int i,T &e) {
    LinkList p=head; int j=1;
    while (p->next&&j<i) {
        p=p->next; j++;
    }
    if (!(p->next)||j>i)
        return 0;
    e=p->next->data;
    return 1;
}

/*
 * 查
 * 若存在,则返回它的位序,否则返回0
 */
int LocateElem_L(LinkList head,T e) {
    LinkList p; int j;
    for (p=head,j=1;p->next;p=p->next,j++) {
        if (p->next->data...) // 满足条件
            break;
    }
    if (!(p->next))
        return 0;
    else
    	return j;
}

/*
 * 逆序输入n个数据元素,建立带头结点的单链表(前插法)
 */
void CreateList_H(LinkList &head,int n) {
    head=(LinkList)malloc(sizeof(LNode));
    head->next=NULL;
    for (int i=0;i<n;i++) {
        LinkList p=(LinkList)malloc(sizeof(LNode));
        scanf("%d",&(p->data));
        p->next=head->next;
        head->next=p;
    }
}

/*
 * 输入n个数据元素,建立带头结点的单链表(后插法)
 */
void CreateList_E(LinkList &head,int n) {
    head=(LinkList)malloc(sizeof(LNode));
    head->next=NULL;
    LinkList pend=head;
    for (int i=0;i<n;i++) {
        LinkList p=(LinkList)malloc(sizeof(LNode));
        scanf("%d",&(p->data));
        p->next=NULL;
        pend->next=p;
        pend=p;
    }
}
 
/*
 * 销毁链表
 */
void DestroyList_L(LinkList &head) {
    LinkList p=head;
    while (p) {
        LinkList q=p;
        p=p->next;
        free(q);
    }
}

链表的优缺点

  1. 插入和删除运算时,无须移动表中元素的位置,只需修改有关结点的指针内容

  2. 不需要一块连续的存储空间,只要能存放一个数据元素的空闲结点就可以被利用

  3. 表的规模易扩充

  4. 不能随机访问表中元素,访问时间与元素在表中的位置有关

改进的单链表

上述定义的单链表中:

问题

  1. 表长是一个隐含的值
  2. 在单链表的最后一个元素之后插入元素时,需遍历整个链表
  3. 元素的“位序”概念淡化,结点的“位置”概念加强

改进

  1. 增加表长、表尾指针和当前位置的指针
  2. 将基本操作中的位序 i 改变为指针 p
// ----------Linklist_V2.h----------
#ifndef LINKLIST_V2_H_
#define LINKLIST_V2_H_

typedef int T; // 数据元素类型

typedef struct LNode {
	T data;
	struct LNode* next;
} LNode,*Link;

typedef struct {
	Link head;
    Link tail;
    Link current;
    int len;
} LinkList;

extern LinkList L;

int InitList_L(LinkList &L);
// 构造一个空的线性链表L,其头指针、尾指针和当前指针均指向头结点,表长为零
// O(1)

void DestroyList_L(LinkList &L);
// 销毁线性链表L,L不再存在
// O(n)

int ListEmpty_L(LinkList L);
// 判表空
// O(1)

int ListLength_L(LinkList L);
// 求表长
// O(1)

int Prior_L(LinkList L);
// 改变当前指针指向其前驱
// O(n)

int Next_L(LinkList L);
// 改变当前指针指向其后继
// O(1)

T GetElem_L(LinkList L);
// 返回当前指针所指数据元素
// O(1)

int LocateElem_L(LinkList L,T e);
// 若存在与e满足函数compare()判定关系的元素,则移动当前指针指向第1个满足条件的元素,返回1,否则返回0
// O(n)

int LocatePos(LinkList L,int i);
// 改变当前指针指向第i个结点
// O(n)

void ClearList_L(LinkList &L);
// 重置L为空表
// O(n)

void PutElem_L(LinkList &L,T e);
// 更新当前指针所指数据元素
// O(1)

int Append(LinkList &L,Link s);
// 在表尾结点之后链接一串结点
// O(s)

int InsAfter(LinkList &L,T e);
// 将元素e插入在当前指针之后
// O(1)

void DelAfter(LinkList &L,T &e);
// 删除当前指针之后的结点
// O(1)

#endif

其它形式的链表

(1) 双向链表

链表中的结点有两个指针域,分别指向后继和前趋

  1. “查询”与单链表相同:Length, Get, Locate 操作仅涉及一个方向的指针
  2. “插入”和“删除”与单链表有区别:双向链表结点可以直接定位前驱,双向链表中需要同时修改两个方向上的指针(画图)
typedef struct DuLNode {
	T data;
    struct DuLNode* prior;
    struct DuLNode* next;
} DuLNode,*DuLinkList;
// 前插
// s指向待插结点
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p->prior=s;

// 后插
// s指向待插结点
s->next=p->next;
p->next=s;
s->next->prior=s;
s->prior=p;

// 前删
// s指向待删结点
s=p->prior;
s->prior->next=s->next;
s->next->prior=s->prior;
free(s);

// 后删
// s指向待删结点
s=p->next;
p->next=s->next;
s->next->prior=s->prior;
free(s);

(2) 循环链表

最后一个结点的指针域的指针又指回第一个结点的链表

和单链表的差别仅在于:判别链表中最后一个结点的条件不再是“后继是否为空”,而是“后继是否为头结点”

(3) 双向循环链表

(4) 静态链表

数组的一个分量表示一个结点,同时用游标(指示器 cur)代替指针指示结点在数组中的位置

数组的第 0 个分量可看成头结点,其指针域指示链表的第一个结点,最后一个结点指针域值为 0

这种存储结构仍需要预先分配一个较大的存储空间,但在作线性表的插入和删除操作时不需移动元素,仅需修改指针,故仍具有链式存储结构的主要优点

  1. 在静态链表中,以整型游标 i 代替动态指针 p,类似于p=p->next,指针后移操作用i=S[i].cur实现

  2. 静态链表中指针修改的操作和单链表基本相同,所不同的是,需由用户自己实现 malloc 和 free 这两个函数

    如何辨明数组中哪些分量未被使用?解决的办法是:将所有未被使用过的以及被删除的分量用游标链成一个备用的链表,每当进行插入时便可从备用链表上取得第一个结点作为待插入的新结点;反之,在删除时将从链表中删除下来的结点链接到备用链表上

#define MAXSIZE 1000 // 链表最大长度

typedef struct {
	T   data;
	int cur; // 指示结点在数组中的相对位置
} SLinkList[MAXSIZE];
// demo
// 在静态单链表S中查找第1个值为e的元素,若找到,则返回它在S中的位序,否则返回0
int LocateElem_SL(SLinkList S,T e) {
	int i=S[0].cur;
    while (i&&S[i].data!=e)
        i=S[i].cur;
    return i;
}

(5) 有序链表

有序链表的操作与一般线性链表基本一致,除了以下两个操作有区别:

bool LocateElem_OL(OLinkList L, T e, Position &q, int(*compare)(T, T))

结果:若有序表 L 中存在元素 e,则 q 指示 L 中第一个值为 e 的元素的位置,并返回函数值 TRUE;否则 q 指示第一个大于 e 的元素的前驱的位置,并返回函数值 FALSE

void OrderInsert(OLinkList L, T e, int(*compare)(T, T))

结果:按有序判定函数 compare 的约定,将值为 e 的结点插入到有序链表 L 的适当位置

4 - 在实际应用中采用哪一种存储结构更合适?

存储空间

对于存储空间的考虑可以用存储密度的大小来衡量。其中存储密度的大小定义为一个结点数据本身所占用的存储量与结点结构所占用的存储量的比值。一般地,存储密度越大,存储空间的利用率就越高。显然,顺序表的存储密度为 1,而链式存储结构的存储密度则小于 1。

顺序表要求预先分配存储空间,一般在程序执行之前是难以估计存储空间大小,估计过大会造成浪费,估计过小又会产生空间溢出。而链式存储结构的存储空间是动态分配,只要内存空间有空间,就可动态申请内存空间,不会产生溢出。

运算时间

顺序存储结构是一种随机存取的结构,即表中任一元素都可在 O(1) 时间复杂度直接地存取。链式存储结构必须从头指针开始顺着链扫描才能取得,一般情况下其时间复杂度为 O(n)。

对于那些只进行查找运算而很少做插入和删除等的运算,宜采用顺序存储结构。对于那些需要频繁地进行元素的插入和删除运算的线性表,其存储结构应采用链式存储结构