数据结构(一)——线性表
数据结构(一)——线性表
1. 顺序表
1.1 顺序表的定义
线性表的顺序存储又称顺序表。它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。第1个元素存储在顺序表的起始位置,第i个元素的存储位置后面紧接着存储的是第i+1个元素,称i为元素ai在顺序表中的位序。因此,顺序表的特点是表中元素的逻辑顺序与其存储的物理顺序相同。
1.2 顺序表的实现
1.2.1 静态分配
静态分配的顺序表存储结构,范围和大小已经事先固定(不可再更改),一旦空间占满,再加入新的数据就会产生溢出,进而导致程序崩溃。
1 | //静态分配的顺序表存储结构 |
1.2.2 动态分配
动态分配的顺序表存储结构,存储数组的空间是在程序执行过程中通过动态存储分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间,将原表中的元素全部拷贝到新空间,从而达到扩充数组存储空间的目的。
1 | //动态分配的顺序表存储结构 |
1.2.3 顺序表的声明
顺序表存储结构建立以后,要在主函数声明一个顺序表变量,顺序表才算建立完成
1 | int main(){ |
1.3 动态存储结构的扩容
动态存储结构的扩容的核心思想就是开辟一个更大的存储空间,将原数据复制进新区域,再释放原存储内存。
需要注意的是,静态分配的顺序存储不可以类比动态存储结构一样扩容。这是因为静态分配的顺序存储是基于静态数组实现的,其大小在声明时就已经确定,并且在程序的整个生命周期(从程序启动到程序结束整个阶段的全过程)内都是固定的,无法改变。
静态如果需要扩容,只能程序员通过更改程序进行扩容(其实就是程序员建了一个存储空间更大的新表)。而动态分配的存储结构如果扩容,可以通过以下程序在程序的生命周期内进行扩容。
1 | //动态存储结构扩容 |
1.4 顺序表的初始化
1.4.1 静态分配存储结构初始化
静态分配初始化简单,只需要将表长设置为0即可。
1 | //初始化表结构 |
1.4.2 动态分配存储结构初始化
动态分配初始化就稍微麻烦点,首先要创建存储分配空间,还要指出存储空间首地址。其次要将表长和最大存储容量初始化。
1 | //注意这里的InitSize,在1.1.2里已经定义,下面程序中两个使用InitSize的地方要保持一致。 |
1.4.3 补充–malloc函数和free函数
malloc函数
malloc
函数用于在堆上动态分配指定字节数的内存空间。其原型如下:
1 | void *malloc(size_t size); |
size
参数表示要分配的字节数。
如果分配成功,malloc
返回一个指向分配的内存的指针;如果分配失败(例如,由于内存不足),则返回NULL
。
使用malloc
分配的内存空间在程序运行期间持续存在,直到使用free
函数显式释放,或者程序结束。
free函数
free
函数用于释放之前通过malloc
(或calloc
、realloc
等)分配的内存空间。其原型如下:
1 | void free(void *ptr); |
ptr
参数是一个指向之前分配的内存的指针。
调用free
后,应将指针设置为NULL
,以防止悬挂指针(dangling pointer),即指向已经被释放的内存的指针。
使用free
释放内存后,这块内存空间就不再属于程序,并且不应再被访问。尝试访问已释放的内存可能导致未定义行为,包括程序崩溃。
1.5 顺序表的插入操作
顺序表的插入操作就是在顺序表L的第i(1<=i<=L.length+1)个位置插入新元素e。若i的输入不合法,则返回false,表示插入失败;否则,将第i个元素及其后的所有元素依次往后移动一个位置,腾出一个空位置插入新元素e,顺序表长度增加1,插入成功,返回true。
1 | //顺序表的插入操作 时间复杂度为O(n) |
1.6 顺序表的删除操作
顺序表的删除操作即将表L中第i(1<=i<=L.length)个位置的元素,用引用变量e返回。若i的输入不合法,则返回false;否则,将被删元素赋给引用变量e,并将第i+1个元素及其后的所有元素依次往前移动一个位置,返回true。
1 | //顺序表的删除操作 时间复杂度O(n) |
1.7 顺序表的查找
1.7.1 按值查找
顺序表的按值查找就是在顺序表L中找第一个元素值等于e的元素,并返回其位序(注意位序是逻辑结构上的位置排序,而非物理结构)。
1 | //顺序表的按值查找操作,时间复杂度O(n) |
1.7.2 按位查找
顺序表的按位查找就是在顺序表L中直接查找指定位置的元素值,并将其返回出来。
1 | //由于顺序表具有随机访问的优点,所以可以根据位序直接查找元素,时间复杂度为O(1) |
2. 单链表
2.1 单链表的定义
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息之外,还需要存放一个指向其后继的指针。
2.2 单链表的实现
单链表结点结构由数据域和指针域组成。数据域可以定义为data,用来存放数据元素;指针域定义为next,用来存放后继结点。
1 | //定义单链表结点类型 |
2.3 单链表的初始化
单链表有两种写法,一种是带头结点的,一种是不带头结点的。具体区别如下图:
对于单链表,不管带不带头结点,头指针都指向单链表的第一个结点。而带头结点的单链表中,头结点作为第一个结点,往往不存储信息。所有带头结点和不带头结点的单链表的初始化操作是不同的。带头结点的单链表初始化时,需要创建一个头结点,并让头指针指向头结点,头结点的next域初始化为NULL。
1 | //带头结点的单链表的初始化 |
不带头结点的单链表初始化时,只需将头指针L初始化为NULL。
1 | //不带头结点的单链表初始化 |
2.4 单链表建立
2.4.1 头插法建立单链表
该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后。具体实现过程如下图所示:
算法实现过程如下:
1 | //头插法建立带头结点的单链表 |
采用头插法建立单链表时,读入数据的顺序与生成的链表中元素的顺序是相反的,可用来实现链表的逆置。每个结点插入的时间为O(1),设单链表长为n,则总时间复杂度为O(n)。
上述采用头插法建立的单链表是带有头结点的,接下来补充一下不带头结点的头插法建立单链表。不在头结点的单链表在进行头插法时,每次插入新结点后,都需要将它的地址赋值给头指针L。
1 | // 头插法建立不带头结点的单链表 |
2.4.2 采用尾插法建立单链表
头插法建立单链表生成的链表中结点的次序和输入数据的顺序不一致。若希望两者次序一致,则可采用尾插法。该方法将新结点插入到当前链表的表尾,为此必须增加一个尾指针r(设置了尾指针r,其时间复杂度就和头插法相同。否则,每次插入都需要遍历查找到最后一个结点进行插入,时间复杂度达到O(n2)),使其始终指向当前链表的尾节点。具体过程如图所示:
算法实现如下:
1 | //尾插法建立带头结点的单链表 |
1 | // 尾插法建立不带头结点的单链表 |
2.5 单链表的查找
2.5.1 按序号查找结点
从单链表的第一个结点开始,沿着next域从前往后依次搜索,直到找到第i个结点为止,则返回该结点的指针;若i小于单链表的表长,则返回NULL。
1 | //按序号查找带头结点的单链表 时间复杂度O(n) |
1 | // 按序号查找不带头结点的单链表,时间复杂度O(n) |
2.5.2 按值查找表结点
从单链表的第一个结点开始,从前往后依次比较表中各结点的数据域,若某结点的data域等于给定值e,则返回该结点的指针;若整个单链表中没有这样的结点,则返回NULL。
1 | //按值查找带表头的表结点 时间复杂度为O(n) |
1 | // 按值查找不带头结点的单链表 时间复杂度为O(n) |
2.6 插入结点操作
插入结点操作将值为x的新结点插入到单链表的第i个位置。先检查插入操作位置的合法性,然后找到待插入位置的前驱,即第i-1个结点,再在其后插入。操作过程如下图所示:
具体算法如下,首先查找第i-1个结点,假设第i-1个结点为 *p,然后令新结点 *s 的指针域指向 *p 的后继,再令结点 *p的指针域指向新插入的结点 *s。
1 | //带头结点的链表的插入操作 时间复杂度为O(n) |
1 | // 不带头结点的链表的插入操作,时间复杂度为O(n) |
在单链表的插入算法中,通常都采用后插操作。但是除了后插操作还可以采用前插操作,前插操作是指在某结点的前插入一个新结点。但是前插操作需要在传递链表时将表头也传输过来,否则在前插操作就会找不到前驱结点的位置。为了解决这个问题,可以采用伪前驱的方式进行插入操作,即将新结点插入需要进行前插结点的后方,然后将两个结点的数据域与指针域都进行交换,便可实现伪前插操作。代码片段如下(将元素e插入p结点前方):
1 | //传入元素e,申请新结点存储元素e |
2.7 删除结点操作
删除结点操作是将单链表的第i个结点删除。先检查删除位置的合法性,然后查找表中第i-1个结点,即被删除结点的前驱,再删除第i个结点。操作过程如下图:
具体算法如下,假设结点 *p 为找到的被删结点的前驱,为实现这一操作后的逻辑关系的变化,仅需修改 *p的指针域,将 *p的指针域next指向 *q的下一结点,然后释放 *q的存储空间。
1 | bool ListDelete(LinkList& L, int i, ElemType& e) |
1 | // 不带头结点的链表的删除操作 |
要删除某个给定结点 *p,通常做法是先从链表头结点开始顺序找到其前驱,然后执行删除操作。除此以外,也可以用删除 *p 的后继来实现,实质就是将其后继的值赋予其自身,然后删除后继。代码片段如下:
1 | q=p->next; //令q指向p的后继结点 |
3. 双链表
3.1 双链表的实现
双链表示意图:
双链表结点类型描述:
1 | //双链表链表结构类型定义 |
3.2 双链表的插入操作
双链表中p所指的结点之后插入结点 *s。过程如下图:
操作代码片段如下:
1 | //将结点s插入p之后 |
3.3 双链表的删除操作
删除双链表中结点 *p的后继 *q。过程如下图:
删除操作代码片段:
1 | p->next = q->next; //步骤1 |
4. 循环链表
4.1 循环单链表
循环单链表和单链表的区别在于,表中最后一个结点的指针不是NULL,而改为指向头结点,从而整个链表形成一个环。在循环单链表中,表位结点 *r的next域指向L,故表中没有指针域为NULL的结点,因此,循环单链表的判空条件是r是否指向L。下图为循环单链表的示意图:
4.2 循环双链表
循环双链表中,多出一个头结点的prior指针还要指向表尾结点。当某结点 *p 为尾结点时,p->next==L;当循环双链表为空表时,其头结点的prior域和next域都等于L。下图为循环双链表的示意图:
5. 静态链表
静态链表是用数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,但与链表不同的是,这里的指针是结点在数组中的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。
静态链表和单链表对应关系如图(静态链表以next==-1作为其结束标志):
静态链表结构类型描述:
1 | //静态链表的最大长度 |