“数组、堆栈”与“链表、队列”的区别是什么?

前言

线性表就是数据排成像一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。其实除了数组,链表、队列、栈等也是线性表结构。而与它相对立的概念是非线性表,比如二叉树、堆、图等。

一、数组

数组(Array)是一种线性表数据结构。它用一组连续的内存空间,来存储一组具有相同类型的数据。

数组可以根据下标随机访问,计算机根据寻址公式可以快速查找下标为i的元素:
a[i]_address = base_address + i() * data_type_size
即下标为i的元素地址=数组首地址+下标 i 乘以 数据类型大小。例如数组中存储的是 int 类型数据data_type_size 就为 4 个字节。数组要求连续的内存空间,所以想在数组中删除、插入一个数据,为了保证连续性,就需要做大量的数据搬移工作。

小结:数组用一块连续的内存空间,来存储相同类型的一组数据,最大的特点就是支持随机访问O(1),但插入、删除操作比较低效,平均情况时间复杂度为 O(n)。

二、链表(Linked list)

链表不需要像数组一样需要一块连续的内存空间,它通过“指针”将一组零散的内存块串联起来使用。

常见的链表结构有:单链表、双向链表和循环链表。通常第一个结点叫作头结点,把最后一个结点叫作尾结点,头结点用来记录链表的基地址。

1、单链表:每个链表的结点除了存储数据之外,还有一个后继指针 next记录下一个结点的地址。尾结点指向一个空地址 NULL。
2、循环链表:循环链表是特殊的单链表。循环链表的尾结点指针是指向链表的头结点。
3、双向链表:每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。

针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以对应的时间复杂度是 O(1)。因为链表中的数据并非连续存储的,想随机访问第 k 个元素,无法像数组那样通过寻址公式就能直接计算出对应的内存地址,而是需要根据指针一个结点一个结点地依次遍历,直到找到相应的结点。

小结:链表是内存不连续的,通过“指针”将零散的内存块串联起来使用的。链表的插入和删除操作比较快都快O(1),随机访问的性能没有数组好O(n),编写代码时注意边界条件,对插入第一个结点和删除最后一个结点的情况进行特殊处理。

三、栈

栈的特点后进先出。栈主要包含两个操作,入栈 push()和出栈 pop(),也就是在栈顶插入一个数据和从栈顶删除一个数据。
数组实现的栈,我们叫作顺序栈,支持动态扩容。用链表实现的栈,我们叫作链式栈
栈的应用:
1、函数调用栈
操作系统给每个线程分配了一块独立的内存空间,用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈

eg:
int main() {
int a = 1;
int ret = 0;
int res = 0;
ret = add(3, 5);
res = a + ret;
printf(“%d”, res);
reuturn 0;
}
int add(int x, int y) {
int sum = 0;
sum = x + y;
return sum;
}
函数栈里出栈、入栈的操作:
在这里插入图片描述

2、表达式求值

eg:3+5*8-6。
实际上,编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。
在这里插入图片描述

小结:栈是一种“操作受限”的线性表,只允许在一端插入和删除数据即入栈和出栈,只需要一个栈顶指针。后进者先出,先进者后出。典型应用场景函数调用,表达式求值也需要理解。入栈、出栈的时间复杂度都为 O(1)。

四、队列

队列的特点先进先出。主要包含两个操作,入队enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列

队空和队满的判定条件:队满的判断条件是 tail == n,队空的判断条件是 head == tail。

在数组实现队列的时候,会有数据搬移操作,要想解决数据搬移的问题,我们就需要像环一样的循环队列。循环队列当队满时,(tail+1)%n=head。其中最后tail 指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。

小结:队列也是一种“操作受限”的线性表,先进者先出,对应入队和出队,队列需要两个指针:一个是 head 指针,指向队头;一个是 tail 指针,指向队尾。注意队空和队满的判定条件,典型应用场景如循环队列、阻塞队列、并发队列、线程池。

五、递归

1、递归需要满足的三个条件:

  • 一个问题的解可以分解为几个子问题的解
  • 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样。
  • 存在递归终止条件。

2、如何编写递归代码?

最关键的是写出递推公式,找到终止条件。

3、为什么递归代码容易造成堆栈溢出呢?

函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成返回时,才出栈。系统栈或者虚拟机栈空间一般都不大。如果递归求解的数据规模很大,调用层次很深,一直压入栈,就会有堆栈溢出的风险。

4、那如何避免出现堆栈溢出呢?

  • 我们可以通过在代码中限制递归调用的最大深度的方式来解决这个问题。
  • 为了避免重复计算,我们可以通过一个数据结构(比如散列表)来保存已经求解过的 f(k)。当递归调用到 f(k)时,先看下是否已经求解过了。如果是,则直接从散列表中取值返回,不需要重复计算。

小结:递归不算数据结构但是一种应用非常广泛的编程技巧,注意使用递归需满足的条件以及如何找出递归公式和终止条件,避免死循环,编码时注意避免堆栈溢出和重复计算。 典型应用如 DFS 深度优先搜索、前中后序二叉树遍历,斐波那契数列。

总结:本篇文章主要总结了一些线性数据结构的特点及优缺点比较,时间空间复杂度分析,以及常用的递归。关于其他非线性结构,图、二叉树、堆等内容,在其他篇幅描述,或可以自行查阅资料类比分析。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只IT攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值