数据结构和算法(一):复杂度、数组、链表、栈、队列

news/2024/5/17 17:56:38/文章来源:https://blog.csdn.net/qq_24252589/article/details/130021182

从广义上来讲:数据结构就是一组数据的存储结构 , 算法就是操作数据的方法

数据结构是为算法服务的,算法是要作用在特定的数据结构上的。

10个最常用的数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie树

10个最常用的算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法

本文总结了20个最常用的数据结构和算法,不管是应付面试还是工作需要,只要集中精力攻克这20个知识点就足够了。

第一章 复杂度

一、什么是复杂度分析?

    1. 数据结构和算法 为了解决 “如何让计算机更快时间、更省空间”问题的。
    1. 因此需从执行时间和占用空间两个维度来评估数据结构和算法的性能。
    1. 分别用时间复杂度空间复杂度两个概念来描述性能问题,二者统称为复杂度。
    1. 复杂度描述的是算法执行时间(或占用空间)与数据规模的增长关系。

二、为什么要进行复杂度分析?

    1. 和性能测试相比,复杂度分析有不依赖执行环境、成本低、效率高、易操作、指导性强的特点。
    1. 掌握复杂度分析,将能编写出性能更优的代码,有利于降低系统开发和维护成本。

三、如何进行复杂度分析?

    1. 大O表示,O表示成正比关系
    • (1)来源

    算法的执行时间与每行代码的执行次数成正比,用T(n) = O(f(n))表示,其中T(n)表示算法执行总时间,f(n)表示每行代码执行总次数,而n往往表示数据的规模

    • (2)特点

    以时间复杂度为例,由于时间复杂度描述的是算法执行时间与数据规模的增长变化趋势,所以常量阶、低阶以及系数实际上对这种增长趋势不产决定性影响,所以在做时间复杂度分析时忽略这些项

    1. 复杂度分析法则
    • (1)单段代码看高频:比如循环。

    • (2)多段代码取最大:比如一段代码中有单循环和多重循环,那么取多重循环的复杂度。

    • (3)嵌套代码求乘积:比如递归、多重循环等

    • (4)多个规模求加法:比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度相加。

四、常用的复杂度级别?

    1. 多项式阶:随着数据规模的增长,算法的执行时间和空间占用按照多项式的比例增长。包括,
      O(1)(常数阶) 、 O(logn)(对数阶)、 O(n)(线性阶)、 O(nlogn)(线性对数阶)、 O(n^2)(平方阶) 、 O(n^3)(立方阶)
    1. 非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。包括,
      O(2^n)(指数阶)、O(n!)(阶乘阶)

五、如何掌握好复杂度分析方法?

复杂度分析关键在于多练,所谓孰能生巧。

第二章 数组

数组看起来很简单,但是很多人没有理解数组的精髓。为什么数组要从0开始编号,而不是从1开始呢? 带着这个问题,开始学习。

一、数组如何实现随机访问的?

    1. 数组是一种线性表数据结构,用连续的存储空间存储相同类型数据
    1. 线性表,顾名思义,就是数据排成像一条线一样的结构,每个线性表上的数据最多只有两个方向:向前和向。
  • 3 .常见的线性表结构有:数组、链表、队列、栈 ,非线性表有:树、图

    1. 数据拥有连续的内存空间,并且存储相同类型的数据,所以我们知道首地址和每个元素的大小,就可以很轻易的访问到数组中的每一个元素,所以利用下标访问数组的时间复杂度为O(1),也就是说利用下标的话,数组可以做到随机访问
数组中 i 元素的地址 = 首地址 + i * 每个元素的大小 (如果数组中存放的是Int类型的,那么data_type_size就是4个字节)a[i]_address = base_address + i * data_type_size
    1. 对数组进行删除插入操作时,为了保证数组内存的连续性,就要做大量的数据搬移工作。(例如:删除第一个元素,就得把后面的元素都往前移动一位,保证内存的连续性)

二、 数组的插入和删除

    1. 数组插入:最好时间复杂度O(1)、 最坏时间复杂度O(n)、 平均时间复杂度O(n)
    • 优化办法:如果数组是无序的话,在第K个位置插入新的元素时,可以将第K个位置元素移动到数组末尾,然后再把新的元素插入到第k个位置,这样处理的话时间复杂度就变成O(1)了。
    1. 数组删除:最好时间复杂度为O(1)、 最坏为O(n)、 平均为O(n)
    • 优化办法:多次删除集中在一起,提高删除效率。记录下已经被删除的数据,每次的删除操作并不是真正的删除数据,只是对被删除的数据做一个标记,当数组没有更多的存储空间时,再触发一次真正的删除操作。(这就是JVM标记清除垃圾回收算法的核心思路)

三、 那么数组的下标为什么从0开始呢?

    1. 从下方的内存地址公式可以看出,下标从0开始比下标从1开始,少了一步减法运算,数组作为最基础的数据结构,使用非常多,为了追求极致的效率,选择了下标从0开始。
下标从0开始,则a[k]的内存地址为:
a[k]_address = base_address + k * type_size下标从1开始,则a[k]的内存地址为:
a[k]_address = base_address + (k - 1) * type_size
    1. 其实主要原因还是历史原因,很多高级语言都效仿C,为了减少学习成本,沿用了下来。

第三章 链表

一、 链表是什么

    1. 链表也是一种线性表数据结构,不像数组一样需要连续的内存空间,链表通过指针将一组零碎的内存块串联起来,我们把内存块称为链表的结点,为了把所有的结点串联起来,每个链表的结点除了存储数据外,还需要记录链表的下一个结点地址,我们把这个记录下一个结点的地址的指针称为后继指针。如图所示:

    1. 我们把第一个结点称之为头结点,把最后一个结点称之为尾结点头结点用来记录链表的基地址单向链表的尾结点的指针指向NULL
    1. 常见的链表结构:单向链表(后继指针指向下一个节点,尾结点的指向NULL)、循环链表(尾结点的后继指针指向头结点的数据,形成一个环)、双向链表(多了一个前驱指针指向前边的节点)、双向循环链表(多前驱指针和尾结点指向头结点)

二、 链表的插入、删除、访问

    1. 链表是零散的内存块,插入和删除的时间复杂度都是O(1)

    1. 链表的访问:时间复杂度为O(n),有利就有弊,链表的访问就没法像数组那么高效了,链表的访问需要根据指针一个节点一个节点的去遍历,所以需要O(n)的时间复杂度。

三、 链表与数组的对比

    1. 内存:数组是连续的内存空间,链表是零碎的内存空间;分配同样大小的内存,数组就有可能报“内存不足”的错误,而链表就有可能成功申请到。(因为数组要求必须连续的内存空间,系统可能没有足够的连续内存空间了)
    1. 插入、删除、随机访问的时间复杂度
    • 链表是零散的内存块,插入和删除的时间复杂度是O(1),查找的时间复杂度是O(n)

    • 数组是连续的内存空间,插入和删除的时间复杂度O(n),按下标查找的时间复杂度是O(1)

    1. 实际工作中如何选取?
    • 如果对内存要求非常苛刻,数组适合你,因为链表的每个节点都需要耗费额外的空间去存放下一个节点的指针,而且对链表频繁的插入、删除,会导致频繁的内存申请和释放,容易造成内存碎片

    • 数组连续的内存空间,借助CPU缓存机制可以预读数据,而链表因为内存的不连续性,所以对CPU缓存不友好,没办法有效预读

    • 数组的缺点是大小固定,一经声明就要占用整块连续的内存空间,可能导致系统没有足够大的空间分配给它,从而导致“内存不足”的错误出现。如果数组过小不够用时,就只能申请一个更大空间的数组,把原有的数组拷贝进去,非常费时。而链表就没有这个限制,链表天然支持扩容

四、 如何基于链表实现LRU缓存淘汰算法?

    1. 缓存可以提高数据读取性能,但是当缓存满了之后,如何清理缓存呢?有三种策略:先进先出策略 FIFO最少使用策略LFU最近最少使用策略LRU(核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”,即删除长期不用的)
    1. LRU缓存淘汰算法的实现
    • 方法一:LRU缓存淘汰算法-链表实现

        1. 如果缓存中已存在,将链表中的此元素删除,并且将新数据放入头结点
        1. 如果不在缓存中:
        • (1). 缓存未满,直接插入到头结点

        • (2). 缓存已满,删除尾结点,将新数据插入到头结点

      • 总时间复杂度:O(n) , 因为查找是否在缓存中需要按个判断

    • 方法二:LRU缓存淘汰算法-数组实现

      • 1.如果数组中已存在此数据,删除数组中的此数据,并将新数据放入数组尾部,时间复杂度为O(n)

      • 2.如果不在缓存中:

        • (1). 缓存未满,插入数组尾部,时间复杂度O(1)

        • (2). 缓存已满,删除数组头元素,并将新数据插入到数组尾部,时间复杂度O(n)

      • 总时间复杂度:O(n)

五、 字符串用单向链表存储,如果判断此字符串是一个回文串呢?(例如:level 返过来也是 level)

    1. 设定一个步长为1的慢指针,和一个步长为2的快指针,当快指针走完整个链表时,慢指针刚好走到一半
    1. 慢指针边走边将后继指针指向前一个节点,造成前半个链表反序
    1. 当慢指针指向中间时,新设定初始化一个指针,指向前半个反序链表的第一个,然逐个与慢指针的数据做比较,如果完全一致,则为回文串,否则不是
    1. 做完对比之后,将前半个反序的链表还原

    总时间复杂度:O(n)
    总空间复杂度:O(1)

六、 练习题

精选了5个常见的链表操作,你只要把这5个练熟,我保证你以后再也不会害怕写链表的代码了。

  • 单链表反转
  • 链表中环的检测
  • 两个有序链表合并
  • 删除链表倒数第n个结点
  • 求链表的中间结点
  • 练习题LeetCode对应编号:206,141,21,19,876,大家可以去练习。

第四章 栈

一、 什么是栈

    1. 栈:后进者先出,先进者后出,这就是典型的栈结构。举个例子:就像叠盘子一样,后来放的盘子总是在上面,拿的时候也是从上面拿,也就是先拿后来放上面的盘子,最后拿最早放的盘子。

    1. 栈是一种受限制的线性表,只允许在一端插入和删除数据。相比于数组和链表,栈带给我们的只有限制,为什么我们要用这个操作受限的呢?
      • 事实上,从功能上来说,数组或者链表确实可以代替栈,但你要知道,特定的数据结构对特定的场景的抽象,数组和链表暴露了太多的操作接口,操作上确实灵活,但是使用时就比较不可控了,自然也就更容易出错。
      • 当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,我们就应该首选这种数据结构。
    1. 栈既可以用数组实现,也可以用链表实现,用数组实现的栈,叫做顺序栈;用链表实现的栈,叫做链式栈
// 一个用数组实现的简单顺序栈
public class ArrayStack{var stack: Array = Array<Any>()// 入栈func push(element: Any){stack.append(element)}// 出栈func pop() -> Any?{guard stack.count > 0 else{ return nil }let lastElement = stack.laststack.removeLast()return lastElement}
}

二、 经典使用场景 - 系统调用栈

  • 操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构,用来存储函数调用时产生的临时变量,每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完毕后返回后,将这个函数对应的栈帧出栈。
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;
}

 上述代码的函数调用栈,如下图所示:

三、 经典使用场景 - 表达式求值 - 也就是如何计算:A*B+C-D/E ?

  • 编译器通过两个栈实现的这个功能的,一个栈保存操作数,另一个栈保存运算符,我们从左到右遍历表达式,当遇到数字时,将其压入操作数栈;当遇到运算符时,就与运算符栈的栈顶元素进行比较。

  • 如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈的栈顶元素的优先级低或相同,那么就从操作数栈的栈顶取2个操作数,然后进行计算,再把计算完成的结果压入操作数栈,继续比较。

  • 下图为3+5*8-6这个表达式的计算过程,可以参照图来理解

四、 经典使用场景 - 如何实现浏览器的前进、后退功能

  • 使用两个栈X和Y,首次浏览的界面入X栈。

  • 后退时,依次从X中出栈,并将出栈的数据依次放入Y栈;前进时,从Y栈中出栈,在依次入X栈。

  • 当X栈中没有数据时,说明不能后退了;当Y栈中没有数据时,说明不能前进了。

第五章 队列

一、什么是队列

    1. 队列:先进者先出,这就是典型的队列。队列有两个基本的操作:入队(enqueue)和出队(dequeue),入队就是把数据放在队列尾部,出队就是把队列头部取一个元素。

    1. 队列和栈一样也是操作受限的线性表结构,和栈一样,队列可以用数组实现,也可以用链表实现,用数组实现的队列叫做顺序队列,用链表实现的队列叫做链式队列

二、 队列的插入和删除

    1. 队列从队头移除元素,从队尾插入元素,大家想象一下,有一个顺序队列,让一个head指针指向队头,让一个tail指向队尾。对这个顺序队列不断的进行插入和删除操作,head和tail指针都会持续往后移动,当tail移动到队尾时,即便数组中有空闲空间,也无法继续往队列中添加数据了。
    1. 对于这个问题应该怎么解决呢?可以进行数据搬移操作,当tail指针指向队尾时,这时的入队操作需要将所有数据搬移到队头开始的地方,这样就轻松解决这个问题了,但是这并不是最佳解决方案,最佳解决方案是循环队列。

顺序队列的数据搬移

三、循环队列

  • 循环队列,顾名思义,它长得像一个环,原本数组是有头有尾的,是一条直线,现在我们把首尾相连,变成一个环形,如下图所示:

  • 循环队列的插入和删除:我们让head指针继续指向队头,而tail指针需要指向队尾后面一个位置,每次在队尾插入,都让tail后移一位,直到tail指向head前边的元素,说明队满了。(为什么是tail指向head前边的元素代表队满呢?而不是tail指向head代表队满呢?如果tail == head,即代表队满,又代表队空,那程序就没法写了)

  • 循环队列队空的条件是:tail == head

  • 循环队列队满的条件是:(tail + 1) % n = head (队满的时候,空了一个内存空间)

 

 

四、阻塞队列和并发队列

    1. 阻塞队列其实就是在队列的基础上增加了阻塞操作,简单来说,就是当队头是空的时候,从队头取元素会被阻塞,因为队列是空的没有数据可取;当队列已经满了的时候,插入操作会被阻塞,直到队列中有空闲位置了,才能继续插入操作。
  • 2.使用阻塞队列,很容易就可以实现一个 “生产者-消费者模型”,可以有效的调度生产和消费的关系。当生产者生产速度过快时,消费者来不及消费,存储数据的队列很快就满了,这个时候生产者就阻塞等待,直到消费者消费了数据,生产者才会被重新唤醒,继续生产。

    1. 我们还可以协调消费者和生产者的个数,提高数据的处理效率,例如:配置多个消费者来应该一个生产者,如下图:

    1. 并发队列,也可以称为线程安全的队列,同一时刻仅允许一个存或取操作,最简单的实现方式就是直接在enqueue()和dequeue()方法上加锁
    1. 基于数组的循环队列,利用CAS原子操作,可以实现非常高效并发队列。这也是循环队列比链式队列应用更加广泛的原因。

五、课后题:写出循环队列代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_283262.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

StorageManagerService.java中的mVold.mount

android源码&#xff1a;android-11.0.0_r21&#xff08;网址&#xff1a;Search (aospxref.com)&#xff09; 一、问题 2243行mVold.mount执行的是哪个mount函数&#xff1f; 2239 private void mount(VolumeInfo vol) { 2240 try { 2241 // TOD…

【LeetCode】-- 108. 将有序数组转换为二叉搜索树

1. 题目 108. 将有序数组转换为二叉搜索树 - 力扣&#xff08;LeetCode&#xff09; 给你一个整数数组 nums &#xff0c;其中元素已经按升序排列&#xff0c;请你将其转换为一棵高度平衡二叉搜索树。高度平衡二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 …

mysql在CentOS7.x环境安装

查看当前环境的yum源 ls -l /etc/yum.repos.d/ 可以看到当前环境是没有下载mysql对应的yum源的, 所以需要去官网下载对应的yum源. 找mysql的yum源并安装 http://repo.mysql.com/ 在选择对应yum源之前, 需要看一下自己系统的版本: 进入官网后, 鼠标右击进入查看页面源代码, 因为…

Leetcode.463 岛屿的周长

题目链接 Leetcode.463 岛屿的周长 easy 题目描述 给定一个 row x col的二维网格地图 grid&#xff0c;其中&#xff1a;grid[i][j] 1表示陆地&#xff0c; grid[i][j] 0表示水域。 网格中的格子 水平和垂直 方向相连&#xff08;对角线方向不相连&#xff09;。整个网格被…

如何从功能测试转型到自动化测试:我三年的学习经历

前言 在软件测试的领域里&#xff0c;自动化测试已经成为了不可或缺的一部分。 与传统的手工测试相比&#xff0c;自动化测试具有更高的效率和精确度&#xff0c;能够有效地减少测试时间和成本&#xff0c;同时提高测试质量。作为一个从事软件测试的人员&#xff0c;如果你想…

Oracle JDK 和 OpenJDK 有什么区别?

可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么 Oracle JDK 和 OpenJDK 之间是否存在重大差异&#xff1f;下面我通过收集到的一些资料&#xff0c;为你解答这个被很多人忽视的问题。 首先&#xff0c;2006 年 SUN 公司将 Java 开源&#xff0c;也就有…

智慧方政务云顶层设计与建设方案(ppt)

本资料来源公开网络&#xff0c;仅供个人学习&#xff0c;请勿商用&#xff0c;如有侵权请联系删除 对一网统管总体架构的理解物联网生态中的业务定位物联网产品与解决方案概览智联物联网管理平台总体方案智联物联网管理平台总体架构智联联连接平台(HLINK)应用架构智慧社区基于…

Linux--进程信号

前言 无人问津也好&#xff0c;技不如人也罢&#xff0c;你都要试着安静下来&#xff0c;去做自己该做的事情&#xff0c;而不是让烦恼和焦虑毁掉你不就不多的热情和定力。心可以碎&#xff0c;手不能停&#xff0c;该干什么干什么&#xff0c;在崩溃中继续努力前行&#xff0c…

export、export default 和import

&#x1f468; 作者简介&#xff1a;大家好&#xff0c;我是Taro&#xff0c;前端领域创作者 ✒️ 个人主页&#xff1a;唐璜Taro &#x1f680; 支持我&#xff1a;点赞&#x1f44d;&#x1f4dd; 评论 ⭐️收藏 文章目录前言一、export default 和 import &#xff1f;1. e…

【教学类-29-03】20230409《门牌号-黏贴版(5层*5间)灰底下划线》-(中班《我爱我家》偏数学)

作品样式&#xff1a; 背景需求 在门牌号黏贴版教学实践中&#xff0c;发现90%的幼儿都不会做 1、空格没有平均分布&#xff1a; 从5*630的门牌号中&#xff0c;随机抽取5个空格&#xff0c;有80%的概率出现“一行2个空、3行1个空”的情况。但幼儿第一次做&#xff0c;楼层都…

软考总结条款(2023-05-28系统分析师)

Raid0、 Raid1、 Raid5、 Raid10的原理、特点、性能区别 - 2023-04-07 指令集 - 2023-04-07 RISC全称Reduced Instruction Set Compute&#xff0c;精简指令集计算机。 CISC全称Complex Instruction Set Computers&#xff0c;复杂指令集计算机。 CISC既有简单指令也有复杂指…

【协议项目之 I2C】(一) 基本时序与实现

一、基本介绍 I2C协议&#xff08;集成电路总线&#xff09;使用两根线SDA和SCL实现数据传输&#xff0c;其连接如下图所示&#xff0c;总线上通过上拉电阻可以挂载各种低速外设,例如EEPROM 24C02,传感器等。   使用I2C&#xff0c;可以将多个从机&#xff08;Slave&#xf…

upload-labs pass6-pass10

1.pass-6黑名单 空格绕过 直接上传肯定不可以 这个地方配置文件虽然只过滤了.htaccess&#xff0c;.user.ini也是不可用的&#xff0c;因为这里进行了重命名&#xff0c;通过代码审计可以发现空格没有过滤&#xff0c;这是利用windows的一个特性&#xff0c;后缀后面有空格和…

EasyCVR在公共资源交易中心监控视频汇聚项目中的场景应用方案

一、背景分析 2019年5月&#xff0c;国务院办公厅印发了《国务院办公厅转发国家发展改革委关于深化公共资源交易平台整合共享实施意见的通知》&#xff08;国办函〔2019〕41号&#xff09;&#xff0c;明确深化公共资源平台整合共享&#xff0c;要求地方各级人民政府制度细化落…

1.8 函数的连续与间断

我的理解&#xff1a; 注意&#xff1a; 在处理连续性问题时&#xff0c;需要注意以下几点&#xff1a; 连续函数在一段区间内的取值具有稳定性和连续性&#xff0c;因此可以使用它们来刻画某个过程的规律。 如果一个函数在某个点处不连续&#xff0c;那么这个点就是一个间断…

C语言预处理命令(宏定义和条件编译)

C语言预处理命令&#xff08;宏定义和条件编译&#xff09; 前言 在编译和链接之前&#xff0c;还需要对源文件进行一些文本方面的操作&#xff0c;比如文本替换、文件包含、删除部分代码等&#xff0c;这个过程叫做预处理&#xff0c;由预处理程序完成。 较之其他编程语言&am…

图像复原论文阅读:GRL算法笔记

标题&#xff1a;Efficient and Explicit Modelling of Image Hierarchies for Image Restoration 会议&#xff1a;CVPR2023 论文地址&#xff1a;http://arxiv.org/abs/2303.00748 官方代码&#xff1a;https://github.com/ofsoundof/GRL-Image-Restoration 作者单位&#xf…

国产化复旦微电子 FMQL45T900 替代Xilinx ZYNQ ARM+FPGA 7045方案(评论区有联系方式)

FM4550国产化开发板 功能接口 - - 系统框图 - - 对应参数 - 1.主要参数 系统1&#xff1a; FPGA型号&#xff1a;FMQL45T900 PS内核&#xff1a;四核ARM Cortex-A7&#xff0c;主频800MHz PS端内存&#xff1a;1GB DDR3,数据速率1066Mbps&#xff0c;32bit PL端内存&…

vagrant无剩余磁盘空间,无法连接Mysql

vagrant无剩余磁盘空间&#xff0c;无法连接Mysql 参考博客1 参考博客2 1.报错&#xff1a;设备上没有剩余空间 C:/HashiCorp/Vagrant/embedded/gems/2.2.19/gems/net-scp-3.0.0/lib/net/scp.rb:398:in await_response_state: \x01scp: /tmp/vagrant-network-entry-eth1-1680…

工业树莓派如何保障电气安全?

一、应用背景 电气系统主要用于传输和分配电力&#xff0c;是工业生产过程中不可或缺的组成部分&#xff0c;广泛应用于工业自动化控制、机器人、电动汽车等领域。因此&#xff0c;实时监测电气系统具有重要意义。 电流是电气系统中最基本的参数之一&#xff0c;实时监测电气…