基本介绍
算法
在计算机领域里,算法是一系列程序指令,用于处理特定的运算和逻辑问题。 衡量算法优劣的主要标准是时间复杂度和空间复杂度。
数据结构
数据结构是数据的组织、管理和存储格式,其使用目的是为了高效地访问和修改数据。 数据结构包含数组、链表这样的线性数据结构,也包含树、图这样的复杂数据结构。
时间复杂度
时间复杂度是对一个算法运行时间长短的量度,用大O表示,记作 T(n)=O(f(n))。 常见的时间复杂度按照从低到高的顺序,包括O(1)、O(logn)、O(n)、 O(nlogn)、O(n2 )等。
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,用大O表 示,记作S(n)=O(f(n))。 常见的空间复杂度按照从低到高的顺序,包括O(1)、O(n)、O(n2 )等。其中递 归算法的空间复杂度和递归深度成正比。
线性数据结构
数组
数组对应的英文是array,是有限个相同类型的变量所组成的有序集合,数组中的每一个变量被称为元素。数组是最为简单、最为常用的数据结构。
数组的另一个特点,是在内存中顺序存储,因此可以很好地实现逻辑上的顺序表。内存是由一个个连续的内存单元组成的,每一个内存单元都有自己的地址。在这些内存单元中,有些被其他数据占用了,有些是空闲的。 数组中的每一个元素,都存储在小小的内存单元中,并且元素之间紧密排列, 既不能打乱元素的存储顺序,也不能跳过某个存储单元进行存储。
数组拥有非常高效的随机访问能力,只要给出下标,就可以用常量时间找到对应元素。有一种高效查找元素的算法叫作二分查找, 就是利用了数组的这个优势。
数组的劣势,体现在插入和删除元素方面。由于数组元素连续紧密地存储在内存中,插入、删除元素都会导致大量元素被迫移动,影响效率。
数组所适合的是读操作多、写操作少的场景
链表
链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。 单向链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部 分是指向下一个节点的指针next
链表的第1个节点被称为头节点,最后1个节点被称为尾节点,尾节点的next指 针指向空。
链表在内存中的存储方式则是随机存储。链表的每一个节点分布在内存的不同位置,依靠next指针关联起来。这样可以灵活有效地利用零散的碎片空间。
插入节点
链表插入节点时,同样分为3种情况。 尾部插入、头部插入、中间插入
尾部插入,把最后一个节点的next指针指向新插入的节点即可。
头部插入,可以分成两个步骤。 第1步,把新节点的next指针指向原先的头节点。 第2步,把新节点变为链表的头节点。
中间插入,同样分为两个步骤。 第1步,新节点的next指针,指向插入位置的节点。 第2步,插入位置前置节点的next指针,指向新节点。
只要内存空间允许,能够插入链表的元素是无穷无尽的,不需要像数组那样考虑扩容的问题。
与数组区别
循环链表
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linkedlist)。
双向链表
双向链表比单向链表稍微复杂一些,它的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev指针。
跳跃表
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳跃表的有关性质:
(1). 跳跃表的每一层都是一条有序的链表.
(2). 跳跃表的查找次数近似于层数,时间复杂度为O(logn),插入、删除也为 O(logn)。
(3). 最底层的链表包含所有元素。
(4). 跳跃表是一种随机化的数据结构。
(5). 跳跃表的空间复杂度为 O(n)。
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。
栈
栈(stack)是一种线性数据结构,栈中的元素只能先入后出(First In Last Out,简称FILO)。最早进入的元素存放的位置叫作栈底(bottom),最后进入的元素存放的位置叫作栈顶(top)。 栈这种数据结构既可以用数组来实现,也可以用链表来实现。
入栈
入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素的位置将会成为新的栈顶。
出栈
出栈操作(pop)就是把元素从栈中弹出,只有栈顶元素才允许出栈,出栈元素的前一个元素将会成为新的栈顶。
队列
队列(queue)是一种线性数据结构。 不同于栈的先入后出,队列中的元素只能先入先出(First In First Out,简称 FIFO)。队列的出口端叫作队头(front),队列的入口端叫作队尾(rear)。
入队
入队(enqueue)就是把新元素放入队列中,只允许在队尾的位置放入元素, 新元素的下一个位置将会成为新的队尾。
出队
出队操作(dequeue)就是把元素移出队列,只允许在队头一侧移出元素,出队元素的后一个元素将会成为新的队头。
循环队列
把队列的这种头尾相接的顺序存储结构称为循环队列。
(队尾下标+1)%数组长度 = 队头下标时,代表此队列真的已经满了。 需要注意的是,队尾指针指向的位置永远空出1位,所以队列最大容量比数组长度小1。
循环队列充分利用了数组的空间,避免了数组元素整体移动
双端队列
对双端队列来说,从队头一端可以入队或出队,从队尾一端也可以入队或出队。
散列表
散列表也叫作哈希表(hash table),这种数据结构提供了键(Key)和值 (Value)的映射关系。只要给出一个Key,就可以高效查找到它所匹配的Value,时间复杂度接近于O(1)。在散列表中,不是直接把键(Key)关键字作为数组的下标,而是根据键(Key)关键字计算出相应的下标。
散列函数
散列函数“将输入映射到数字”,散列函数必须满足一些要求。
- 它必须是一致的。
- 它应将不同的输入映射到不同的数字。
如果输入的关键字是整数,则一般合理的方法就是直接返回 Key mod Tablesize。通常是保证表的大小是素数。当输入的关键字是随机整数时,散列函数不仅计算起来简单而且关键字的分配也很均匀。
如果输入的关键字是字符串,可以把字符串中字符的 ASCII码(或 Unicode码)值加起来。实现起来简单而且能够很快地计算出答案
哈希冲突
如果当一个元素被插入时与一个已经插入的元素散列到相同的值,那么就产生一个冲突,这个冲突需要消除。解决这种冲突的方法有几种,其中最简单的两种:分离链接法和开放定址法。
分离链接法
其做法是将散列到同一个值的所有元素保留到一个链表中。
开放定址法
一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
树
在数据结构中,树的定义如下。 树(tree)是n(n≥0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点。
- 有且仅有一个特定的称为根的节点。
- 当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每一个集合本身 又是一个树,并称为根的子树。
树的最大层级数,被称为树的高度或深度。
二叉树
二叉树(binary tree)是树的一种特殊形式。是n(n=O)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
=
二叉树还有两种特殊形式,一个叫作满二叉树,另一个叫作完全二叉树。
满二叉树
一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上,那么这个树就是满二叉树。
完全二叉树
对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树。
二叉树物理存储结构
1. 链式存储结构。链式存储是二叉树最直观的存储方式。
2.数组。使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上。 如果某一个节点的左孩子或右孩子空缺,则数组的相应位置也空出来。
二叉树遍历方法
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
二叉树的遍历分为4种。 1. 前序遍历。 2. 中序遍历。 3. 后序遍历。 4. 层序遍历
前序遍历 二叉树的前序遍历,输出顺序是根节点、左子树、右子树。
二叉树的中序遍历,输出顺序是左子树、根节点、右子树
二叉树的后序遍历,输出顺序是左子树、右子树、根节点。
层序遍历,顾名思义,就是二叉树按照从根节点到叶子节点的层次关系,一层 一层横向遍历各个节点
二叉排序树
二叉排序树(Binary Sort Tree),又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值:
- 它的左、右子树也分别为二叉排序树。
构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度。不管怎么说,在一个有序数据集上的查找,速度总是要快于无序的数据集的,而二叉排序树这种非线性的结构,也有利于插入和删除的实现。
总之,二叉排序树是以链表的方式存储,保持了链表存储结构在执行插入或删除操作时不用移动元素的优点,只要找到合适的插入和删除位置后,仅需修改链表指针即可。插入删除的时间性能比较好。而对于二叉排序树的查找,走的就是从根结点到要查找的结点的路径,其比较次数等于给定值的结点在二叉排序树的层数。极端情况,最少为1次,即根结点就是要找的结点,最多也不会超过树的深度。也就是说,二叉排序树的查找性能取决于二叉排序树的形状。
平衡二叉树(AVL树)
AVL( Adelson- Velskii 和 Landis)树是一种二叉排序树,其中每一个节点的左子树和右子树的高度差至多等于1。是带有平衡条件(balance condition)的二叉查找树。我们将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子 BF(Balance Factor),那么平衡二叉树上所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。
平衡二叉树构建的基本思想就是在构建二叉排序树的过程中,每当插入一个结点时,先检查是否因插入而破坏了树的平衡性,若是,则找出最小不平衡子树。在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。
如果我们需要查找的集合本身没有顺序,在频繁查找的同时也需要经常的插入和删除操作,显然我们需要构建一棵平衡二叉树,我们的查找时间复杂度就为O(logn),而插入和删除也为O(logn)。
多路查找树(B树)
多路查找树(muitl-way search tree),其每一个结点的孩子数可以多于两个,且每一个结点处可以存储多个元素。由于它是查找树,所有元素之间存在某种特定的排序关系。
一个m阶的B树具有如下几个特征:
1.根结点至少有两个子女。
2.每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
4.所有的叶子结点都位于同一层。
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
在一个典型的B树应用中,要处理的硬盘数据量很大,因此无法一次全部装入内存。因此我们会对B树进行调整,使得B树的阶数(或结点的元素) 与硬盘存储的页面大小相匹配。比如说一棵B树的阶为1001(即1个结点包含 1000 个关键字),高度为2,它可以储存超过10亿个关键字,我们只要让根结点持久地保留在内存中,那么在这棵树上,寻找某一个关键字至多需要两次硬盘的读取即可。 通过这种方式,在有限内存的情况下,每一次磁盘的访问我们都可以获得最大数量的数据。由于B 树每结点可以具有比二叉树多得多的元素,所以与二又树的操作不同,它们减少了必须访问结点和数据块的数量,从而提高了性能。 B树,是为磁盘存储而专门设计的一类平衡搜索树。
B+树
一个m阶的B+树具有如下几个特征:
- 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
- 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
- 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
B+树的优势**:**
- 单一节点存储更多的元素,使得查询的IO次数更少。
- 所有查询都要查找到叶子节点,查询性能稳定。
- 所有叶子节点形成有序链表,便于范围查询。
红黑树
红黑树是一棵二叉搜索树,它在每个结点上增加了一个存储位来表示结点的颜色,可以是RED或 BLACK。通过对任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,因而是近似于平衡的。 树中每个结点包含5个属性:color、 key、 left、right 和p。如果一个结点没有子结点或父结点,则该结点相应指针属性的值为NIL。我们可以把这些NIL视为指向二叉搜索树的叶结点(外部结点)的指针,而把带关键字的结点视为树的内部结点。
一棵红黑树是满足下面红黑性质的二叉搜索树:
1.每个结点或是红色的,或是黑色的。
2.根结点是黑色的。
3.每个叶结点(NIL)是黑色的。
4.如果一个结点是红色的,则它的两个子结点都是黑色的。
5.对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。
红黑树(red black tree)是许多“平衡”搜索树中的一种,红黑树的高度最多是2log(N+1)。因此,查找操作保证是一种对数的操作,可以保证在最坏情况下基本动态集合操作的时间复杂度为O(logn)。
二叉堆
二叉堆是一个数组,它可以被看成一个近似的完全二叉树。 树上的每一个结点对应数组中的一个元素。除了最底层外,该树是完全充满的,而且是从左向右填充。表示堆的数组A包括两个属性:A. length(通常)给出数组元素的个数,A. heap-size 表示有多少个堆元素存储在该数组中。 1. 最大堆。 2. 最小堆。
最大堆
最大堆的任何一个父节点的值,都大于或等于它左、右孩子节点的值。
最小堆
最小堆的任何一个父节点的值,都小于或等于它左、右孩子节点的值。
二叉堆的根节点叫作堆顶。 最大堆和最小堆的特点决定了:最大堆的堆顶是整个堆中的最大元素;最小堆的堆顶是整个堆中的最小元素。
优先队列
优先队列分为两种情况。 最大优先队列,最小优先队列
在最大优先队列中,无论入队顺序如何,当前最大的元素都会优先出队,这是基于最大堆实现的。
在最小优先队列中,无论入队顺序如何,当前最小的元素都会优先出队,这是基于最小堆实现的。
图
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(VE),其中,G 表示一个图,V是图G中顶点的集合,E是图G中边的集合。
图的定义:
- 线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(Vertex)。
- 线性表中可以没有数据元素,称为空表。树中可以没有结点,叫做空树。 在图结构中,不允许没有顶点。在定义中,若V是顶点的集合,则强调了顶点集合V有穷非空。 线性表中,相邻的数据元素之间具有线性关系,
- 树结构中,相邻两层的结点具有层次关系,而图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
参考
漫画算法:小灰的算法之旅
大话数据结构
数据结构动态可视化