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

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

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

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.last
        stack.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.mfbz.cn/a/8105.html

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

相关文章

办公协作效率想提质增效,可借助开源大数据工具!

在信息爆炸式发展的今天&#xff0c;提升办公协作效率&#xff0c;让各部门的信息有效互通起来&#xff0c;做好数据管理&#xff0c;已经成为众企业提升竞争力的方式方法。那么&#xff0c;如果想要提升办公效率&#xff0c;就需要了解开源大数据工具了。在数字化发展进程中&a…

《扬帆优配》西藏地震!美史上最严排放新规将出台,美股收涨

当地时间周四&#xff0c;美股遍及收高&#xff0c;科技股领涨。因耶稣受难日&#xff0c;美股4月7日&#xff08;周五&#xff09;休市&#xff0c;周四为美股本周最终一个买卖日&#xff0c;从本周状况来看&#xff0c;纳指与标普500指数均录得跌幅&#xff0c;别离跌1.1%和0…

回归预测 | MATLAB实现PSO-RF粒子群算法优化随机森林多输入单输出回归预测

回归预测 | MATLAB实现PSO-RF粒子群算法优化随机森林多输入单输出回归预测 目录回归预测 | MATLAB实现PSO-RF粒子群算法优化随机森林多输入单输出回归预测效果一览基本介绍程序设计参考资料效果一览 基本介绍 MATLAB实现PSO-RF粒子群算法优化随机森林多输入单输出回归预测 粒子…

第十四届蓝桥杯题解

声明&#xff1a;以下都无法确定代码的正确性&#xff0c;是赛时代码&#xff0c;希望大家见谅&#xff01;思路可以参考&#xff0c;等后续可以评测之后再去修改博客内错误&#xff0c;也希望大家能够指正错误&#xff01; 试题A&#xff1a;日期统计 分析&#xff1a;这道题…

45-Dockerfile-ARG/ENV指令

AGR/ENV指令前言ARG作用格式说明生效范围使用示例ENV作用格式说明使用环境变量使用示例ARG 和 ENV 的区别前言 本篇来学习下Dockerfile中的AGR/ENV指令 ARG 作用 定义一个可以在构建镜像时使用的变量 格式 ARG <name>[<default value>]说明 在执行 docker b…

【数据结构】AVL树

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《数据结构与算法》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; AVL树&#x1f332;AVL树&#x1f334;AVL树的插入&#x1f334;AVL树的旋转左单旋右单旋左…

Springboot基础学习之(十三):通过代码实现对数据库的增删该查操作(数据库:mysql)

Springboot这个系列实现的案例&#xff1a;员工后台管理系统 之前讲解的内容是前后端的交互&#xff0c;并没有涉及到数据库。把员工信息放置在一个数组中&#xff0c;实现的方法则是对数组的增删改查操作&#xff0c;但是从今天开始&#xff0c;实现的功能则是在数据库的基础上…

看完这篇 教你玩转渗透测试靶机vulnhub——My File Server: 2

Vulnhub靶机My File Server: 2渗透测试详解Vulnhub靶机介绍&#xff1a;Vulnhub靶机下载&#xff1a;Vulnhub靶机安装&#xff1a;Vulnhub靶机漏洞详解&#xff1a;①&#xff1a;信息收集&#xff1a;②&#xff1a;FTP匿名登入③&#xff1a;SSH私钥登入④&#xff1a;SMB共享…

Mysql一条多表关联SQL把CPU打爆了,如何优化

今天是清明假期的第三天&#xff0c;收到同事的求助&#xff0c;DB的CPU被打爆了&#xff01; 查看监控&#xff0c;CPU已经被打爆100% 登录mysql&#xff0c;DB无锁阻塞&#xff0c;元凶是一个异常sql,存在39个并发执行。 SQL的明细如下&#xff1a; select TEMPSALE.USER_ID…

ChatGPT 被大面积封号,到底发生什么了?

意大利数据保护机表示 OpenAI 公司不但非法收集大量意大利用户个人数据&#xff0c;没有设立检查 ChatGPT 用户年龄的机制。 ChatGPT 似乎正在遭遇一场滑铁卢。 3月31日&#xff0c; 大量用户在社交平台吐槽&#xff0c;自己花钱开通的 ChatGPT 账户已经无法登录&#xff0c;更…

港科夜闻|香港科大(广州)熊辉教授获委任为协理副校长(知识转移)

关注并星标每周阅读港科夜闻建立新视野 开启新思维1、香港科大(广州)熊辉教授获委任为协理副校长(知识转移)。在任期间&#xff0c;熊教授将为香港科大知识转移战略发展提供全面领导&#xff0c;鼓励和促进教师、学生和校友之间的知识转移&#xff0c;促进创业发展、技术研究及…

数据仓库、数据集市、数据湖,你的企业更适合哪种数据管理架构?

建设企业级数据平台&#xff0c;首先需要了解企业数据&#xff0c;确认管理需求&#xff0c;并选择一个数据管理架构。那么面对纷繁复杂的数据来源&#xff0c;多元化的数据结构&#xff0c;以及他们的管理使用需求&#xff0c;企业数据平台建设该从何处入手呢&#xff1f;哪个…

机器学习笔记之正则化(二)权重衰减角度(直观现象)

机器学习笔记之正则化——权重衰减角度[直观现象]引言回顾&#xff1a;拉格朗日乘数法角度观察正则化过拟合的原因&#xff1a;模型参数的不确定性正则化约束权重的取值范围L1L_1L1​正则化稀疏权重特征的过程权重衰减角度观察正则化场景构建权重衰减的描述过程权重衰减与过拟合…

ChatGPT常用prompts汇总

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️&#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

一个开源人的心酸哭诉

编者按&#xff1a;这篇文章比较长&#xff0c;但值得一读。从这篇文章&#xff0c;你可以看到一些开源开发者的内心活动&#xff0c;看看他们的热情、抱负、无奈、心酸、愤怒、失望和痛斥&#xff1b;看看他们如何指责、讽刺、乞求、羞辱那些使用他们作品而无视他们痛苦的人&a…

Java 泛型 使用案例

参考资料 Java 基础 - 泛型机制详解路人甲-Java泛型专题 目录一. 通用Mapper1.1 实体类1.2 Mapper基类1.3 自定义接口1.4 抽象基类Service1.5 调用二. session和bean的获取一. 通用Mapper 1.1 实体类 ⏹ Accessors(chain true): 允许链式调用 import lombok.Data; import …

Python 进阶指南(编程轻松进阶):五、发现代码异味

原文&#xff1a;http://inventwithpython.com/beyond/chapter5.html 导致程序崩溃的代码显然是错误的&#xff0c;但是崩溃并不是发现程序问题的唯一手段。其他迹象可能表明程序存在更微妙的错误或不可读的代码。就像气体的味道可以指示气体泄漏或者烟雾的味道可以指示火灾一样…

[国产化]arm架构的服务器虚拟机软件---Qemu虚拟机

项目场景&#xff1a; 架构&#xff1a;arm 服务器型号&#xff1a;昆泰r522 操作系统&#xff1a;欧拉22.3arm 问题描述 对应需要有arm架构的虚拟机&#xff0c;vmware没有arm版本。寻找替代品。 之前有人介绍可以用zstack。但是需要改变操作系统。所以就放弃了。选择了&…

Github采用Http Push失败

Github采用Http Push失败 Github的密码凭证从2021年起开始就不能用了&#xff0c;现在采用http去push代码时候提示输入的密码要换成令牌&#xff08;token&#xff09;才可以。 如何在Github上生成自己的令牌呢&#xff1f; &#xff08;1&#xff09;简单来说就是将原来输入…

Spring AOP及事务说明

目录 1.事务管理 1.1 事务说明 1.2 Spring事务管理 1.3 事务进阶 (1)Transactional属性说明 (2)rollbackFor属性 (3)propagation属性 1.4 总结 2.AOP 2.1 AOP概述 2.2 AOP核心概念 2.3 AOP进阶 (1) 通知类型 (2)切点表达式 (3) 通知顺序 (4)连接点 1.事务管理 …
最新文章