数据结构与算法--线性表
线性表介绍
将具有“一对一”关系的数据“线性”地存储到物理空间中,这种存储结构就称为线性存储结构(简称线性表)。
线性表具有以下特点
* 有限的序列
* 使用线性表存储的数据必须具有相同的数据类型。即要么全都是整形,要么全都是字符串。一半是整形,一半是字符串的一组数据无法使用线性表存储。
* 可以是有序的也可以是无序的。可以把线性表理解成一队学生,可以让这些学生根据身高从小到大排列,也可以随机排成一列
线性表常用术语
* 某一元素的左侧相邻元素称为“直接前驱”,位于此元素左侧的所有元素都统称为“前驱元素”;
* 某一元素的右侧相邻元素称为“直接后继”,位于此元素右侧的所有元素都统称为“后继元素”;
存储结构
线性表的存储结构有顺序存储结构和链式存储结构两种,前者称为顺序表,后者称为链表。
1.顺序表
顺序表就是将数据依次存储在连续的整块物理空间中。
顺序表存储数据同数组非常接近。其实,顺序表存储数据使用的就是数组。
// 顺序表的结构体定义
typedef struct Table{
int * head;//声明了一个名为head的长度不确定的数组,也叫“动态数组”
int length;//记录当前顺序表的长度
int size;//记录顺序表分配的存储容量
}table;
顺序表的优点
* 无须为表示表中元素之间的逻辑关系而增加额外的存储空间
* 可以快速地存取表中任一位置的元素
顺序表的缺点
* 插入和删除操作需要移动大量元素
* 当线性表长度变化较大时,难以确定存储空间的容量
* 造成存储空间碎片
2.链表
链表就是数据分散的存储在物理空间中,通过一根线保存着它们之间的逻辑关系。
顺序表最大的缺点就是插入和删除某一元素时都需要移动大量的元素,这是很耗费时间的。这是由于相邻两元素的存储位置具有相邻关系,他们在内存中的位置也是挨着的,
中间没有空虚,不能直接进行插入,要想进行插入,需要先把其他元素进行挪动,同理,若删除某个元素以后,就会流出空隙,也是需要移动其他元素进行补齐。
为了改善顺序表的缺点,让元素之间的位置不必相邻,内存中的位置也不必相邻,我们把这种存储结构称为链式存储结构。
链表中每个数据的存储都由以下两部分组成:
1) 数据元素本身,其所在的区域称为数据域;
2) 指向直接后继元素的指针,所在的区域称为指针域;
2.1 单链表
链表的每个结点中只包含一个指针域,所以叫做单链表。
有的链表带有头结点,有的不带头结点,头节点的数据域可以不存储任何信息,可以存储线性表长度等附加信息,头节点的指针域存储指向第一个结点的指针。
当链表是带有头结点的时候,就相当于火车头一样的存在,只是用来表明列车顺序开始的方向,并不乘坐客人。(链表一般都是包含头结点的)
单链表结点定义
typedef struct LNode
{
int data; //data中存放结点数据域
struct LNode *next; //指向后继结点的指针
}LNode;
2.2 静态链表
用数组描述的链表,即称为静态链表。
在C语言中,静态链表的表现形式即为结构体数组,结构体变量包括数据域data和游标cur。
// 代码实现
#include <stdio.h>
#define MAXSIZE 1000
typedef int ElemType; /* 定义数据元素类型,类型名为ElemType,此处所定义的数据元素只包含一个int型的数据项*/
typedef struct {
ElemType data; // 数据
int cur; // 游标(Cursor)
}Component, StaticLinkList[MAXSIZE];
2.3 循环链表
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
2.4 双向链表
在单链表的基础上,再在每个结点中设置一个指向其前驱结点的指针域,这样一个结点既可以指向它的前面又可以指向它的下一个,我们把这种链表称为双向链表。
typedef struct DLLNode{
int data; //data中存放结点数据域(默认是int类型,也可以是其他)
struct DLNode *prior; //指向前驱结点的指针
struct DLNode *next; //指向后继结点的指针
}DLNode; //定义双链表结点类型
3. 顺序存储和链式存储比较
3.1 存储方式:
* 顺序存储:相邻元素的存放地址也相邻(逻辑与物理统一),内存地址是连续的。
* 链式存储:相邻元素可随意存放。逻辑上相邻的数据元素,物理存储位置不一定相邻,它使用指针(引用)实现元素之间的逻辑关系。
3.2 比较:
基于空间的比较
* 顺序表的存储空间是静态分配的。需要预先分配足够大的存储空间,估计过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。
* 链表的存储空间是动态分配的。
存储密度 = 结点数据本身所占的存储量/结点结构所占的存储总量
顺序表的存储密度 = 1
链表的存储密度 < 1
基于时间的比较
* 顺序表插入/删除时,平均移动表中的一半元素,因此对n较大的顺序表效率低。
* 链表插入/删除时,不需要移动元素,只需要修改指针。
3.3 使用:
1.基于存储空间的考虑
顺序表的存储空间是静态分配的,所以在程序执行前必须确定它的存储规模,因此,当线性表的长度变化较大或者难以估计其存储规模时,宜采用动态链表作为存储结构。
当线性表的长度变化不大而且事先容易确定其大小时,为节省存储空间,则采用顺序表作为存储结构比较适宜。
2.基于运算的考虑(时间)
顺序表适合做查找这样的静态操作;链表适合做插入删除这样的动态操作
* 若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;
* 若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。
表操作
表的操作其实主要分为几种:查找、插入、删除
1. 顺序表操作:
1.1 按元素值的查找算法:
//查找函数,其中,elem表示要查找的数据元素的值
int selectTable(table t, int elem) {
int i;
for (i = 0; i < t.length; i++) {
if (t.head[i] == elem) {
return i + 1;
}
}
return -1;//如果查找失败,返回-1
}
1.2 插入:
//插入函数,其中,elem为插入的元素,pos为插入到顺序表的位置
table posTable(table t, int elem, int pos)
{
int i;
//判断插入本身是否存在问题(如果插入元素位置比整张表的长度+1还大(如果相等,是尾随的情况),或者插入的位置本身不存在,程序作为提示并自动退出)
if (pos > t.length + 1 || pos < 1) {
printf("插入位置有问题");
return t;
}
//做插入操作时,首先需要看顺序表是否有多余的存储空间提供给插入的元素,如果没有,需要申请
if (t.length == t.size) {
t.head = (int *)realloc(t.head, (t.size + 1) * sizeof(int)); // 注意,动态数组额外申请更多物理空间使用的是 realloc 函数
if (!t.head) {
printf("存储分配失败");
return t;
}
t.size += 1;
}
//插入操作,需要将从插入位置开始的后续元素,逐个后移
for (i = t.length - 1; i >= pos - 1; i--) {
t.head[i + 1] = t.head[i];
}
//后移完成后,直接将所需插入元素,添加到顺序表的相应位置
t.head[pos - 1] = elem;
//由于添加了元素,所以长度+1
t.length++;
return t;
}
1.3 删除:
table delTable(table t, int pos) {
int i;
if (pos > t.length || pos < 1) {
printf("被删除元素的位置有误");
exit(0);
}
//删除操作
for (i = pos; i < t.length; i++) {
t.head[i - 1] = t.head[i];
}
t.length--;
return t;
}
2. 链表操作:
2.1 查找:
//p为原链表,elem表示被查找元素、
int selectElem(link * p, int elem) {
//新建一个指针t,初始化为头指针 p
link * t = p;
int i = 1;
//由于头节点的存在,因此while中的判断为t->next
while (t->next) {
t = t->next;
if (t->elem == elem) {
return i;
}
i++;
}
//程序执行至此处,表示查找失败
return -1;
}
2.2 插入:
同顺序表一样,向链表中增添元素,根据添加位置不同,可分为以下 3 种情况:
插入到链表的头部(头节点之后),作为首元节点;
插入到链表中间的某个位置;
插入到链表的最末端,作为链表中最后一个数据元素;
代码示例
//p为原链表,elem表示新数据元素,pos表示新元素要插入的位置
link * insertElem(link * p, int elem, int pos) {
link * temp = p;//创建临时结点temp
link * c = NULL;
int i = 0;
//首先找到要插入位置的上一个结点
for (i = 1; i < pos; i++) {
if (temp == NULL) {
printf("插入位置无效\n");
return p;
}
temp = temp->next;
}
//创建插入结点c
c = (link*)malloc(sizeof(link));
c->elem = elem;
//向链表中插入结点
c->next = temp->next;
temp->next = c;
return p;
}
2.3 删除:
从链表中删除数据元素需要进行以下 2 步操作:
将结点从链表中摘下来;
手动释放掉结点,回收被结点占用的存储空间;
代码示例:
//p为原链表,pos为要删除元素的值
link * delElem(link * p, int pos) {
link * temp = p;
link * del = NULL;
int i = 0;
//temp指向被删除结点的上一个结点
for (i = 1; i < pos; i++) {
temp = temp->next;
}
del = temp->next;//单独设置一个指针指向被删除结点,以防丢失
temp->next = temp->next->next;//删除某个结点的方法就是更改前一个结点的指针域
free(del);//手动释放该结点,防止内存泄漏
return p;
}
3. 静态链表操作:
3.1 查找:
代码示例:
// 静态链表查找只能通过遍历静态链表的方式,查找存有指定数据元素的节点。
// 在以body作为头结点的链表中查找数据域为elem的结点在数组中的位置
int selectNum(component * array, int body, int num) {
//当游标值为0时,表示链表结束
while (array[body].cur != 0) {
if (array[body].data == num) {
return body;
}
body = array[body].cur;
}
//判断最后一个结点是否符合要求
if (array[body].data == num) {
return body;
}
return -1;//返回-1,表示在链表中没有找到该元素
}
3.2 添加:
//向链表中插入数据,body表示链表的头结点在数组中的位置,pos表示插入元素的位置,num表示要插入的数据
void insertArr(component * array, int body, int pos, int num) {
int tempBody = body;//tempBody做遍历结构体数组使用
int i = 0, insert = 0;
//找到要插入位置的上一个结点在数组中的位置
for (i = 1; i < pos; i++) {
tempBody = array[tempBody].cur;
}
insert = mallocArr(array);//申请空间,准备插入
array[insert].data = num;
array[insert].cur = array[tempBody].cur;//新插入结点的游标等于其直接前驱结点的游标
array[tempBody].cur = insert;//直接前驱结点的游标等于新插入结点所在数组中的下标
}
//提取分配空间
int mallocArr(component * array) {
//若备用链表非空,则返回分配的结点下标,否则返回0(当分配最后一个结点时,该结点的游标值为0)
int i = array[0].cur;
if (array[0].cur) {
array[0].cur = array[i].cur;
}
return i;
}
3.3 删除:
//删除结点函数,num表示被删除结点中数据域存放的数据,函数返回新数据链表的表头位置
int deletArr(component * array, int body, int num) {
int tempBody = body;
int del = 0;
int newbody = 0;
//找到被删除结点的位置
while (array[tempBody].data != num) {
tempBody = array[tempBody].cur;
//当tempBody为0时,表示链表遍历结束,说明链表中没有存储该数据的结点
if (tempBody == 0) {
printf("链表中没有此数据");
return;
}
}
//运行到此,证明有该结点
del = tempBody;
tempBody = body;
//删除首元结点,需要特殊考虑
if (del == body) {
newbody = array[del].cur;
freeArr(array, del);
return newbody;
}
else
{
//找到该结点的上一个结点,做删除操作
while (array[tempBody].cur != del) {
tempBody = array[tempBody].cur;
}
//将被删除结点的游标直接给被删除结点的上一个结点
array[tempBody].cur = array[del].cur;
//回收被摘除节点的空间
freeArr(array, del);
return body;
}
}
//备用链表回收空间的函数,其中array为存储数据的数组,k表示未使用节点所在数组的下标
void freeArr(component * array, int k) {
array[k].cur = array[0].cur;
array[0].cur = k;
}
4. 双链表操作:
4.1 查找:
//head为原双链表,elem表示被查找元素
int selectElem(line * head, int elem) {
//新建一个指针t,初始化为头指针 head
line * t = head;
int i = 1;
while (t) {
if (t->data == elem) {
return i;
}
i++;
t = t->next;
}
//程序执行至此处,表示查找失败
return -1;
}
4.2 添加:
//data 为要添加的新数据,add 为添加到链表中的位置
line * insertLine(line * head, int data, int add) {
//新建数据域为data的结点
line * temp = (line*)malloc(sizeof(line));
temp->data = data;
temp->prior = NULL;
temp->next = NULL;
//插入到链表头,要特殊考虑
if (add == 1) {
temp->next = head;
head->prior = temp;
head = temp;
}
else {
int i = 0;
line * body = head;
//找到要插入位置的前一个结点
for (i = 1; i < add - 1; i++) {
body = body->next;
if (body == NULL) {
printf("插入位置有误\n");
break;
}
}
if (body) {
//判断条件为真,说明插入位置为链表尾
if (body->next == NULL) {
body->next = temp;
temp->prior = body;
}
else {
body->next->prior = temp;
temp->next = body->next;
body->next = temp;
temp->prior = body;
}
}
}
return head;
}
4.3 删除:
//删除结点的函数,data为要删除结点的数据域的值
line * delLine(line * head, int data) {
line * temp = head;
//遍历链表
while (temp) {
//判断当前结点中数据域和data是否相等,若相等,摘除该结点
if (temp->data == data) {
temp->prior->next = temp->next;
temp->next->prior = temp->prior;
free(temp);
return head;
}
temp = temp->next;
}
printf("链表中无该数据元素\n");
return head;
}
参考
https://juejin.im/post/5b5130c6f265da0f84561fcd
http://data.biancheng.net/view/157.html
浙公网安备 33010602011771号