算法|从0开始学,不涉及代码(小白必背基础)

202 阅读16分钟

算法

一.算法和数据结构

数据结构是算法的基石,算法如lol英雄,数据结构如峡谷。

算法

  • 运算

  • 查找

  • 排序

  • 最优决策

  • 面试


数据结构

数据结构是数据的组织,管理和存储格式,目的是高效地访问和修改数据。

  • 线性结构(最简单)

    • 数组

    • 链表

      • 队列

      • 哈希表

  • 树(相对复杂)

    • 二叉树
  • 图(更复杂)

  • 其他

时间复杂度

运行时间的长短和占用内存空间的大小,是衡量程序好坏的重要因数。

而我们只能预估代码的基本操作执行次数。

求导取极限,趋于无穷大时,T(n)/f(n)的极限值为不等于0的常数,则曾为同数量级函数。

简单来说,时间复杂度就是把程序的相对执行时间函数T(n)简化为一个数量级,这个数量级是n,n2,n3等

  • 运行时间是常数量级,则用1表示

  • 只保留时间函数中的最高阶项

  • 如果最高阶项存在,则省区最高阶前面的系数

空间复杂度

  • 常量空间O(1)

  • 线性空间O(n)

  • 二维空间O(n^2)

  • 递归空间O(n)

大多数情况下,时间复杂度更为重要一些,宁可牺牲一些空间,也要提升程序执行速度。

小结

  • 什么是算法

    在计算机领域中,算法是一系列程序指令,用于处理特定的运算和逻辑问题。

    衡量算法的优劣主要标准是时间复杂度空间复杂度

  • 什么是数据结构

    数据结构是数据的组织,管理和存储格式,其使用目的是为了高效的访问和修改数据。

    数据结构包含数组,链表,这样的线性数据结构,也包含树,图这样的复杂数据结构。

  • 什么是时间复杂度

    时间复杂度是对一个算法运行时间长短的量度,用大O表示,记作T(n)=O(f(n))。

    常见的时间复杂度按照从低到高的顺序,包括O(1),O(logn),O(n),O(nlogn),O(n^2)。

  • 什么是空间复杂度

    空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,用大O表示,记作S(n)=O(f(n))。

    常见的空间复杂度按照从低到高的顺序,包括O(1),O(n),O(n…^2)等,其中递归算法的空间复杂度和递归深度成正比。

二.数据结构基础

什么是数组(array)(正规军)

是有限个相同类型的变量所组成的有序集合,数组中的每一个变量被称为元素。数组是最简单,最常用的数据结构。

数组适合的是读操作多,写操作少的场景。

什么是链表(地下党)

链表是一种在物理上非连续,非顺序的数据结构,由若干节点组成。

数组vs列表

查找更新插入删除
数组O(1)O(1)O(n)O(n)
链表O(n)O(1)O(1)O(1)

从表格可以看出,数组的优势在于能够快速定位元素。适合读操作多,写操作少的场景

链表的优势在于能够灵活进行插入和删除。适合在尾部频繁插入,删除元素,用列表更合适

栈和队列

物理结构和逻辑结构

栈和队列。这两者都属于逻辑 结构,它们的物理实现既可以利用数组,也可以利用链表来完成

栈(stack)是一种线性数据结构(类似与羽毛球桶

  • 栈中的元素只能先入后出

  • 最早进入的元素存放的位置叫作栈底

  • 最后进入 的元素存放的位置叫作栈顶

栈的基本操作

  1. 入栈

入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入 元素,新元素的位置将会成为新的栈顶

  1. 出栈

出栈操作(pop)就是把元素从栈中弹出,只有栈顶元素才允许出 栈,出栈元素的前一个元素将会成为新的栈顶

无论是以数组还是以链表实现,入栈、 出栈的时间复杂度都是O(1)

队列

队列(queue)是一种线性数据结构(类似于隧道

  • 队列中的元素只能先入先出

  • 队列的出口端叫作队头

  • 队列的入 口端叫作队尾

用数组实现时,为了入队操作的方便,把队尾位置规定为最后入队元素的下一个位置。

队列的基本操作

对于链表实现方式,队列的入队、出队操作和栈是大同小异的。

但对于数组实现方式来说,队列的入队和出队操作有了一些有趣的变化。

  1. 入队

入队(enqueue)就是把新元素放入队列中,只允许在队尾的位置 放入元素,新元素的下一个位置将会成为新的队尾。

  1. 出队

出队操作(dequeue)就是把元素移出队列,只允许在队头一侧移 出元素,出队元素的后一个元素将会成为新的队头。

用数组实现的队列可以采用循环队列的方式来维持队列容量的恒定。

在数组不做扩容的前提下,如何让新元素入队并确定新的队尾位置呢?我们可以利用已出队元素留下的空间,让队尾指针重新指回数组的首位。

一直到(队尾下标+1)%数组长度 = 队头下标时,代表此队列真 的已经满了

队尾指针指向的位置永远空出1位,所以 队列最大容量比数组长度小1。

栈和队列的应用

栈的应用

栈的输出顺序和输入顺序相反,所以栈通常用于对“历史”的回溯, 也就是逆流而上追溯“历史”

实现递归的逻辑,就可以用栈来代替,因为栈可以回溯方法的 调用链。

栈还有一个著名的应用场景是面包屑导航,使用户在浏览页面时可 以轻松地回溯到上一级或更上一级页面

队列的应用

队列的输出顺序和输入顺序相同,所以队列通常用于对“历史”的回 放,也就是按照“历史”顺序,把“历史”重演一遍。

例如在多线程中,争夺公平锁的等待队列,就是按照访问顺序来决 定线程在队列中的次序的

再如网络爬虫实现网站抓取时,也是把待抓取的网站URL存入队列 中,再按照存入队列的顺序来依次抓取和解析的。

双端队列

双端队列这种数据结构,可以说综合了栈和队列的优点,对双端队 列来说,从队头一端可以入队或出队,从队尾一端也可以入队或出队。

优先队列

它遵循的不是先入先出,而是谁的优先级最高,谁 先出队。

优先队列已经不属于线性数据结构的范畴了,它是基于二叉堆来实现的。

散列表 (哈希表)

这种数据结构提供了键 (Key)和值(Value)的映射关系。只要给出一个Key,就可以高效查 找到它所匹配的Value,时间复杂度接近于O(1)。

哈希函数

散列表在本质上也是一个数组

我们需要一个“中转站”,通过某种方式, 把Key和数组下标进行转换。这个中转站就叫作哈希函数。

这里以Java的 常用集合HashMap为例,来看一看哈希函数在Java中的实现

最简单的转化方式是什么呢?是按照数组长度进行取模运算

index = HashCode (Key) % Array.length

散列表的读写操作

  1. 写操作(put)

写操作就是在散列表中插入新的键值对(在JDK中叫作Entry)

由于数组的长度是有限的,当插入的Entry越来越多时,不 同的Key通过哈希函数获得的下标有可能是相同的

这种情况,就叫作哈希冲突

哈希冲突是无法避免的,既然不能避免,我们就要想办法来解决。解决哈希冲突的方法主要有两种 , 一种是开放寻址法,一种是链表法。

  • 开放寻址法

    当一个Key通过哈希函数获得对应的数 组下标已被占用时,我们可以“另谋高就”,寻找下一个空档位置

  • 链表法

    HashMap数组的每一个元素不仅是一个Entry对象,还是一个链表的 头节点。每一个Entry对象通过next指针指向它的下一个Entry节点。当新 来的Entry映射到与之冲突的数组位置时,只需要插入到对应的链表中 即可

  1. 读操作(get)

读操作就是通过给定的 Key,在散列表中查找对应的Value。

在上图中,首先查到的节点Entry6的Key是002947,和待查找的Key 002936不符。接着定位到链表下一个节点Entry1,发现Entry1的Key 002936正是我们要寻找的,所以返回Entry1的Value即可。

  1. 扩容

当经过多次元素插入,散列表达到一定饱和度时,Key映射位置发生冲突的概率会逐渐提高。这样一来,大量元素拥挤在相同的数组下标位置,形成很长的链表,对后续插入操作和查询操作的性能都有很大影响。

   对于JDK中的散列表实现类HashMap来说,影响其扩容的因素有两个。
  • Capacity,即HashMap的当前长度

  • LoadFactor,即HashMap的负载因子,默认值为0.75f

衡量HashMap需要进行扩容的条件如下。

HashMap.Size >= Capacity×LoadFactor

  扩容不是简单地把散列表的长度扩大,而是经历了下面两个步骤。

1. 扩容,创建一个新的Entry空数组,长度是原数组的2倍。 2. 重新Hash,遍历原Entry数组,把所有的Entry重新Hash到新数组 中。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变

散列表可以说是数组和链表的结合,它在算法中 的应用很普遍,是一种非常重要的数据结构

小结

  • 什么是数组

数组是由有限个相同类型的变量所组成的有序集合,它的物理存储方式是顺序存储,访问方式是随机访问。利用下标查找数组元素的时间复杂度是O(1),中间插入、删除数组元素的时间复杂度是O(n)

  • 什么是链表

链表是一种链式数据结构,由若干节点组成,每个节点包含指向下一节点的指针。链表的物理存储方式是随机存储,访问方式是顺序访问。查找链表节点的时间复杂度是O(n),中间插入,删除节点的时间复杂度是O(1)。

  • 什么是栈

栈是一种线性存储结构,可以用数组和链表实现。栈包含入栈和出栈操作,遵循先入后出的原则。

  • 什么是队列

队列也是一种线性逻辑结构,可以用数组和链表实现。队列包含入队和出队操作,遵循先入先出原则。

  • 什么是散列表

散列表也叫哈希表,是存储key- Value映射的集合。对于某一个key,散列表可以在接近O(1)的时间进行读写操作。散列表通过哈希函数实现key和数组下标的转换,通过开放寻址法链表法解决哈希冲突。

三.树

什么是树

除人与人之间的关系之外,许多抽象的东西也可以成为一个“树”, 如一本书的目录。

在数据结构中,树的定义如下。

树(tree)是n(n≥0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点。

  1. 有且仅有一个特定的称为的节点。

  2. 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身又是一个树,并称为根的子树。

什么是二叉树

二叉树(binary tree)是树的一种特殊形式。二叉,顾名思义,这种树的每个节点最多有2个孩子节点。注意,这里是最多有2个,也可能 只有1个,或者没有孩子节点。

二叉树节点的两个孩子节点,一个被称为左孩子(left child),一 个被称为右孩子(right child)。这两个孩子节点的顺序是固定的,就像人的左手就是左手,右手就是右手,不能够颠倒或混淆。

此外,二叉树还有两种特殊形式,一个叫作满二叉树,另一个叫作 完全二叉树

什么是满二叉树

一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上,那么这个树就是满二叉树。

简单点说,满二叉树的每一个分支都是满的

什么是完全二叉树

对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号 为从1到n。如果这个树所有节点和同样深度的满二叉树的编号为从1到n 的节点位置相同,则这个二叉树为完全二叉树

完全二叉树的条件没有满二叉树那么苛刻:满二叉树要求所有分支 都是满的;而完全二叉树只需保证最后一个节点之前的节点都齐全即可

二叉树的应用

主要的应用还在于进行查找操作和维持相对顺序这两个方面。

  1. 查找

二叉查找树的主要作用就是进行查找操作。

二叉查找树在二叉树的基础上增加了以下几个条件。

  • 如果左子树不为空,则左子树上所有节点的值均小于根节点的值
  • 如果右子树不为空,则右子树上所有节点的值均大于根节点的值
  • 左、右子树也都是二叉查找树

对于一个节点分布相对均衡的二叉查找树来说,如果节点总数是 n,那么搜索节点的时间复杂度就是O(logn),和树的深度是一样的。

这种依靠比较大小来逐步查找的方式,和二分查找算法非常相似。

  1. 维持相对顺序

二叉查找树要求左子树小于父节点,右子树大于父节点,正是这样保证了二叉树的有序性。

因此二叉查找树还有另一个名字——二叉排序树(binary sort tree)

二叉树的自平衡。二叉树自平衡 的方式有多种,如红黑树、AVL树、树堆等

二叉树的遍历

从节点之间位置关系的角度来看,二叉树的遍历分为4种

  1. 前序遍历。
  2. 中序遍历。
  3. 后序遍历。
  4. 层序遍历

从更宏观的角度来看,二叉树的遍历归结为两大类。

  1. 深度优先遍历(前序遍历、中序遍历、后序遍历)。
  2. 广度优先遍历(层序遍历)。 下面就来具体看一看这些不同的遍历方式。

深度优先遍历

深度优先和广度优先这两个概念不止局限于二叉树,它们更是一种抽象的算法思想,决定了访问某些复杂数据结构的顺序

  1. 前序遍历(根左右)

二叉树的前序遍历,输出顺序是根节点、左子树、右子树。

  1. 中序遍历(左根右)

二叉树的中序遍历,输出顺序是左子树、根节点、右子树

  1. 后序遍历

二叉树的后序遍历,输出顺序是左子树、右子树、根节点

绝大多数可以用递归解决的问题,其实都可以用另一种数据结构来解决,这种数据结构就是栈。因为递归和栈都有回溯的特性。

广度优先遍历

  1. 层序遍历

层序遍历,顾名思义,就是二叉树按照从根节点到叶子节点的层次 关系,一层一层横向遍历各个节点。

使用队列实现

什么是二叉堆

什么是二叉堆? 二叉堆本质上是一种完全二叉树,它分为两个类型。

  1. 最大堆。
  2. 最小堆

什么是最大堆呢?最大堆的任何一个父节点的值,都大于或等于它 左、右孩子节点的值。

什么是最小堆呢?最小堆的任何一个父节点的值,都小于或等于它 左、右孩子节点的值

二叉堆的根节点叫作堆顶。

最大堆和最小堆的特点决定了:最大堆的堆顶是整个堆中的最大元素;最小堆的堆顶是整个堆中的最小元素。

二叉堆的自我调整

对于二叉堆,有如下几种操作。

  1. 插入节点。

  2. 删除节点。

  3. 构建二叉堆

  4. 插入节点(时间复杂度是O(logn))

当二叉堆插入节点时,插入位置是完全二叉树的最后一个位置

  1. 删除节点(时间复杂度是O(logn))

二叉堆删除节点的过程和插入节点的过程正好相反,所删除的是处 于堆顶的节点

  1. 构建二叉堆(O(n))

构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质 就是让所有非叶子节点依次“下沉”。

二叉堆的代码实现

二叉堆虽然是一个完全二叉树,但它的存储方式并不是链式存储,而是顺序存储。换句话说,二叉堆的所有节点都存储在数组中。

在数组中,在没有左、右指针的情况下,如何定位一个父节点的左孩子和右孩子呢?可以依靠数组下标来计算。 假设父节点的下标是parent,那么它的左孩子下标就是 2×parent+1;右孩子下标就是2×parent+2。

二叉堆是实现堆排序及优先队列的基础。

什么是优先队列

优先队列的特点

待续