一、线性表的定义和基本操作
1.线性表的定义
本节所讲的线性表是一种逻辑结构,表示元素之间一对一的相邻关系。具体实现为顺序表和链表,是指存储结构。
线性表的特点:
- 表中元素的个数有限
- 表中元素具有逻辑上的顺序性,表中元素有其先后次序
- 表中元素都是数据元素,每个元素都是单个元素
- 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间
- 表中元素具有抽象性,即仅讨论元素间的逻辑关系
2.线性表的基本操作
对于大部分数据结构来说,基本操作就是增、删、改、查、创建、销毁。
L:线性表 e:值 i:位序
- InitList(&L):初始化线性表。即构造一个空的线性表,不放内容
- Length(L):求表长。传入一个线性表L,返回L的长度,即L中元素个数
- LocateElem(L,e):按值查找操作。在表L中查找是否存在值为e的元素。在顺序表中,可以获取值为e的元素在线性表中的位置。具体实现按照返回值类型确定。
- GetElem(L,i):按位查找操作。获取L中第i个位置上的元素值。
- ListInsert(&L,i,e):插入操作。向表L中的第i个位置插入值为e的元素,并返回一个新的线性表(操作后的)L。
- ListDelete(&L,i,&e):删除操作。删除表L第i个位置上的元素,并用e返回删除的元素值,返回新表L。
- PrintList(L):输出操作。按顺序输出顺序表的所有元素值。
- isEmpty(L):判空操作。判断线性表是否为空表,若空返回true,否则返回false。
- DestroyList(&L):销毁操作。销毁线性表L,并释放L所占用的内存空间。
【注】:
1.此处使用到的“&”符号表示c++中的引用,目的是可以将修改过后的变量返回。使用指针效果相同
2.只有动态分配的变量和对象才可以使用new和delete
二、线性表的顺序表示
1.顺序表的定义
线性表的顺序存储方式称为顺序表。
顺序表中逻辑顺序与物理顺序相同,它使用了一组地址连续的存储单元,因而存储位置可以用一个简单的公式表示,可以实现随机存取。
顺序表特点:需要连续的存储空间,可以随机访问;存储密度高,每个节点只存储元素;插入和删除操作需要移动大量元素;拓展容量不方便
位序:位置顺序=下标+1
顺序表可以有静态分配和动态分配两种方式。静态分配主要就是数组,动态分配是当数据空间占满时再开辟另一块空间。
顺序表实现查找操作(按位)较为简单
2.基本操作的实现
【注】:所有方法的实现最开始都需要有是否合法/存在等等系列条件的判定,之后再实现操作。
顺序表定义:
#define InitSize 10
typedef int Datatype;
//顺序表结构声明
typedef struct LinearList {
Datatype* data; //数据元素数组指针
int MaxSize; //最多存放元素个数
int Length; //表长
}LinearList;
初始化操作:
//初始化顺序表
void InitList(LinearList& list) {
//list.data = (int*)malloc(sizeof(int) * InitSize); c语言表示法
list.data = new Datatype[InitSize]; //c++表示法
list.MaxSize = InitSize;
list.Length = 0;
}
返回表长:
int Length(LinearList list) {
return list.Length;
}
按值查找元素:O(n)
//按值查找元素,找到则返回下标,否则返回-1
int LocateElem(LinearList list, Datatype e) {
for (int i = 0; i < list.Length; i++) {
if (list.data[i] == e) {
return i;
}
}
return -1;
}
按位查找元素:O(1)
//按位查找元素,找到则返回值,否则返回-1
Datatype GetElem(LinearList list, int i) {
if (i<0 || i>list.Length) {
return -1;
}
return list.data[i];
}
插入操作:O(n)
void ListInsert(LinearList& list, int i, Datatype e) {
//i值不合法
if (i < 0 || i > list.Length) {
return;
}
for (int j = list.Length; j > i; j--) { //j停止循环时,下标在i的后一位
list.data[j] = list.data[j - 1]; //令后面的值等于前面的值
}
list.data[i] = e;
list.Length++; //长度+1
}
删除操作:O(n)
//删除操作
void ListDelete(LinearList& list, int i, Datatype& e) {
if (i < 0 || i >= list.Length) {
return;
}
for (int j = i; j < list.Length - 1; j++) {
list.data[j] = list.data[j + 1];
}
e = list.data[i];
list.Length--; //长度-1
}
遍历输出:O(n)
void PrintList(LinearList list) {
for (int i = 0; i < list.Length; i++) {
cout << list.data[i] << endl;
}
}
判空操作:
bool isEmpty(LinearList list) {
return (list.Length > 0) ? false : true;
}
销毁操作:
void DestroyList(LinearList& list) {
//只有动态分配的部分需要delete(new出来的部分)
delete list.data;
}
【注】:delete可以销毁的内存只有动态分配给出的部分,其余不需要使用delete清除
三、线性表的链式表示
1.单链表的定义
线性表的链式存储又称为单链表。
每个节点除了存放元素自身信息外,还存放一个指向后继的指针。
单链表的逻辑顺序与物理顺序不同,他通过“链”的方式建立起数据元素之间的逻辑关系。
单链表通过一组任意的存储单元来存储线性表中的数据元素,因而元素在存储空间上的分布是离散的,单链表是非随机存取的存储结构,不能直接找到表中某个特定节点。
单链表实现插入删除操作较为简单
2.基本操作的实现
单链表通常有两种表示形式:“头指针”和“头指针+头结点”。一般使用后者。
使用头结点的优点在于将非空表和空表的处理实现了统一。
(以下所有实现均采用带头结点的方式)
单链表定义:
typedef int Datatype;
//单链表结构声明
//由于单链表实际上是由一个个节点链接而成的,因而只需要写出各个节点的结构即可
typedef struct LinkNode {
Datatype data; //数据域
struct LinkNode* next; //指针域(结构体指针)
}LinkNode;
//注:结构体指针才可以使用new关键字
初始化单链表:
//初始化单链表——创建头结点
LinkNode* InitList() {
LinkNode* head = new LinkNode; //创建一个新节点
head->next = NULL;
return head; //返回头节点
}
头插法建表:O(n)
//头插法建表
void HeadInsert(LinkNode* head, Datatype value) {
LinkNode* node = new LinkNode;
node->data = value;
node->next = head->next;
head->next = node;
}
尾插法建表:O(n)
//尾插法建表
void TailInsert(LinkNode* tail, Datatype value) { //tail为尾指针
LinkNode* node = new LinkNode;
node->data = value;
tail->next = node;
node->next = NULL;
tail = node;
}
求表长:
//求表长
int Length(LinkNode* head) {
LinkNode* p = head->next; //辅助指针p,
int length = 0;
while (p != NULL) {
length++;
p = p->next;
}
return length;
}
【注】:单链表长度不包括头结点
按值查找:O(n)
//按值查找
LinkNode* LocateElem(LinkNode* head, Datatype e) {
LinkNode* p = head->next;
while (p != NULL) {
if (p->data == e) {
return p;
}
p = p->next;
}
return NULL;
}
按序号查找:O(n)
//按序号查找
LinkNode* GetElem(LinkNode* head, int i) {
if (i < 0) {
return NULL;
}
if (i == 0) {
return head; //序号为0的节点为头结点
}
LinkNode* p = head;
int j = 0;
while (p != NULL && j != i) {
j++;
p = p->next;
}
return p;
}
插入操作:O(n)——未给定节点/O(1)——给定节点
//插入操作 插入操作要找到插入位置的前驱节点,也就是第i-1个节点
bool Listinsert(LinkNode* head, int i, Datatype e) {
LinkNode* pre = GetElem(head, i - 1); //获取插入位置的前驱节点
if (pre == NULL) {
return false;
}
LinkNode* node = new LinkNode;
node->data = e;
node->next = pre->next;
pre->next = node;
return true;
}
【扩展】:以上代码进行的是后插操作,需要找到插入位置的前驱节点。若要进行前插操作,则可以将其转换为后插操作实现。还是一样的逻辑,找到前驱节点进行插入,再将两个节点的值互换即可。
删除操作:O(n)——未给定节点/O(1)——给定节点
//删除操作
bool ListDelete(LinkNode* head, int i) {
LinkNode* pre = GetElem(head, i - 1); //获取删除位置的前驱节点
if (pre == NULL || pre->next == NULL) {
return false;
}
LinkNode* q = pre->next;
pre->next = q->next;
delete(q);
return true;
}
【扩展】:再删除节点时,我们通常找到其前驱节点进行删除操作。而这需要遍历整个单链表,时间复杂度为O(n)。我们可以将其转换为删除下一个节点,只需要将后继节点的值赋予自身,再删除后继节点即可。这样的时间复杂度为O(1)。
遍历输出:
//输出操作
void PrintList(LinkNode* head) {
LinkNode* p = head->next;
while (p != NULL) {
cout << p->data << endl;
p = p->next;
}
}
判空操作:
//判空操作
bool isEmpty(LinkNode* head) {
return !head->next;
}
销毁操作:
//销毁操作
void DestroyList(LinkNode* head) {
LinkNode* p = head;
while (p != NULL) {
head = p;
delete(head);
p = p->next;
}
delete p;
}
【注】:
1.顺序表包含了许多属性,因此需要写出顺序表一整个结构体;而链表是由一个个节点构成的,因此只需要写出节点结构体即可
2.只要有遍历,一般都需要创建一个辅助指针
3.在这些操作中,如果向某个方法传入了头结点head指针并对其进行了修改,那么就和&引用类型一样,已经在内存中进行修改,无需返回指针变量,所以返回的值可以设置成其他需要的类型
使用&和*效果相同 &针对一整个结构体,*针对某一个指针/节点
要是创建了结构体变量,要对结构体中的各属性初始化,就使用&引用
要是创建了结构体指针变量,并对指针中的各属性初始化,使用*即可
3.双链表
对于单链表,插入删除操作若不给定节点则只能从头结点依次顺序地向后遍历,且不能够快速的对前驱节点进行操作。
【注】:这里对前驱节点操作不方便,可以转换成对后继节点进行操作,或者使用双链表
双链表克服了单链表的以上问题,每个节点除了data数据域和next指针域,还添加了指向前驱节点的prior指针域。它的优点在于可以方便的找到前驱节点。
双链表的查找操作与单链表逻辑相同,但插入和删除操作有不同。
插入操作:(需要改变四个指针)
//插入操作
void DListInsert(DLinkNode* head,int i,Datatype e) {
DLinkNode* pre = DGetElem(head, i - 1);
if (pre == NULL) {
return;
}
DLinkNode* node = new DLinkNode;
node->data = e;
node->next = pre->next;
node->next->prior = node;
node->prior = pre;
pre->next = node;
}
删除操作:(需要改变两个指针)
//删除操作
void DDeleteList(DLinkNode* head, int i) {
DLinkNode* pre = DGetElem(head, i - 1);
if (pre == NULL || pre->next == NULL) {
return;
}
DLinkNode* q = pre->next;
pre->next = q->next;
if (q->next != NULL) {
q->next->prior = pre;
}
delete(q);
}
4.循环链表
循环单链表在单链表的基础上,将最后一个节点的next指针指向头结点。因此循环单链表中没有指针域为空的节点。
循环单链表的判空条件也与单链表有所不同。若循环单链表的头结点next指针等于自身,则为空。
循环单链表的优点在于它可以从任意一个节点开始遍历整个链表。
循环双链表就是双链表+循环链表。在循环双链表中,表尾节点的next指针域要指向头结点,头结点的prior指针域要指向尾结点。
因此循环双链表的判空条件就是头结点的next和prior是不是都等于头结点。
5.静态链表
静态链表借助数组来描述线性表的链式结构,同样需要预先分配一块连续的存储空间。他的数据域与链表相同,指针域改为游标(即下一个元素的数组下标)。当next==-1时,链表结束。
静态链表主要应用于不支持指针的高级语言中。
6.*顺序表和链表的比较
①比较
存取方式:顺序表支持顺序、随机存取;链表只能从头遍历,顺序存取
逻辑结构与物理结构:二者均能反映数据间的逻辑关系,只是顺序表的逻辑结构与物理结构相同,链表不同。
查找、插入和删除:
按值查找:顺序表若有序,则可以使用查找算法提高效率。否则两者时间复杂度均为O(n)。
按位查找:顺序表O(1),链表O(n)。
插入删除:顺序表O(n),链表O(1)。
②选择
基于存储:若数据数量变动不大基本固定,则可以采用顺序表,顺序表存储密度大。数量变动大的采用链表,链表存储密度小,且含有指针域,更占内存。
基于运算:若需要频繁的进行插入和删除操作,选用链表。若经常需要按照位序查找元素,采用顺序表。
---------------持续更新中---------------
(欢迎大佬批评指正!)