重温数据结构和算法

目录

一.简介

二.算法复杂度

1.时间复杂度

2.空间复杂度

三.线性表

1.数组

2.链表

3.栈

4.队列

四.散列表

五.递归和二分查找

递归

二分查找

六.树

树的种类较多如下图:

二叉树

满二叉树

完全二叉树

二叉树的存储

二叉树的遍历

1. 深度优先遍历

2. 广度优先遍历

红黑树

多路树

七.图

图的概念

图的存储

图的遍历

深度优先搜索(DFS,Depth First Search)

广度优先搜索(BFS,Breadth First Search)

八.算法思维

贪心算法

分治算法

回溯算法

动态规划


算法和数据结构(重温数据结构和算法)

一.简介

  当今中型、大型互联网公司面试中,算法和数据结构是必考题,工作中如果遇到底层组件开发良好的数据结构和算法设计也必不可少,因此学好数据结构和算法作为程序员是必修的内功。所谓工欲善其事必先利其器,下面这边文章会介绍一些理论知识附加一些经典案例。

常见数据结构:

 

常见算法:

 

二.算法复杂度

1.时间复杂度

O复杂度表示法看看什么是时间复杂度:

int sum(int n){

    int s=0; //t

    int i=1; //t

    for(;i<=n;i++){ //t*n

      s=s+i;    //t*n

   }

    return s;    //t

 }

n=100

1+1+100n+100n+1=200n+3  //通常省略系数,常数,算出时间复杂度为n

我们假设执行一行代码的时间为t,通过估算,代码的执行时间T(n)与执行次数成正比,记做: T(n)=O(f(n))

T(n): 代码执行时间

n:数据规模

f(n):每行代码执行次数总和

O:代码的执行时间与f(n)表达式成正比

上面的例子中的T(n)=O(2n+2)

当n无限大时,低阶、常量、系统都可以忽略

所以T(n)=O(n)

即上例中的时间复杂度为O(n),也就是代码执行时间随着数据规模的增加而增长。

计算时间复杂度的技巧:

计算循环执行次数最多的代码

总复杂度=量级最大的复杂度

举个例子计算下复杂度是多少:

如下代码

i = 1;

while(i <= n){

  i = i * 2;// 执行最多

}

2 ^x=n

x=log 2n

忽略系数为logn

T(n)=O(logn)

如果将该代码执行n遍

则时间复杂度记录为:T(n)=O(n*logn),即O(nlogn)

快速排序、归并排序的时间复杂度都是O(nlogn)

2.空间复杂度

  空间复杂度全称是渐进空间复杂度,表示算法的存储空间与数据规模之间的增长关系

比如将一个数组拷贝到另一个数组中,就是相当于空间扩大了一倍:T(n)=O(2n),忽略系数。即为: O(n),这是一个非常常见的空间复杂度,比如跳跃表、hashmap的扩容此外还有:O(1),比如原地排序、O(n ) 此种占用空间过大由于现在硬件相对比较便宜,所以在开发中常常会利用空间来换时间,比如缓存技术典型的数据结构中空间换时间是:跳跃表在实际开发中我们也更关注代码的时间复杂度,而用于执行效率的提升。

三.线性表

1.数组

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

时间复杂度

  读取和更新都是随机访问,所以是O(1)

插入数组扩容的时间复杂度是O(n),插入并移动元素的时间复杂度也是O(n),综合起来插入操作的时间复杂度是O(n)。

删除操作,只涉及元素的移动,时间复杂度也是O(n)

优缺点

优点:

    数组拥有非常高效的随机访问能力,只要给出下标,就可以用常量时间找到对应元素

缺点:

    插入和删除元素方面。由于数组元素连续紧密地存储在内存中,插入、删除元素都会导致大量元素被迫移动,影响效率。 (ArrayList LinkedList )申请的空间必须是连续的,也就是说即使有空间也可能因为没有足够的连续空间而创建失败如果超出范围,需要重新申请内存进行存储,原空间就浪费了。

应用

数组是基础的数据结构,应用太广泛了,ArrayList、Redis、消息队列等等。

2.链表

概念

    链表(linked list)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。链表中数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。(百度百科)

 

常见的链表包括:单链表、双向链表、循环链表

单链表:

    单向链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next。

 

双向链表:

   双向链表的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev指针。

 

循环链表:

  链表的尾节点指向头节点形成一个环,称为循环链表。

 

存储原理

    数组在内存中的存储方式是顺序存储(连续存储),链表在内存中的存储方式则是随机存储(链式存储)。链表的每一个节点分布在内存的不同位置,依靠next指针关联起来。这样可以灵活有效地利用零散的碎片空间。

3.栈

概念

   栈(stack)是一种线性数据结构,栈中的元素只能先入后出(First In Last Out,简称FILO)。

最早进入的元素存放的位置叫作栈底(bottom),最后进入的元素存放的位置叫作栈顶 (top)。

存储原理

   栈既可以用数组来实现,也可以用链表来实现

   栈的数组实现如下:

 

数组实现的栈也叫顺序栈或静态栈

栈的链表实现如下:

 

链表实现的栈也叫做链式栈或动态栈

4.队列

概念

    队列(queue)是一种线性数据结构,队列中的元素只能先入先出(First In First Out,简称 FIFO)。队列的出口端叫作队头(front),队列的入口端叫作队尾(rear)。

存储原理

   队列这种数据结构既可以用数组来实现,也可以用链表来实现。

数组实现

 

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

用数组实现的队列叫作顺序队列。

链表实现

 

用链表实现的队列叫作链式队列

四.散列表

概念

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

哈希函数

散列表在本质上也是一个数组。散列表的Key则是以字符串类型为主的。通过hash函数把Key和数组下标进行转换。作用是把任意长度的输入通过散列算法转换成固定类型、固定长度的散列值。

什么是Hash冲突(碰撞)?

由于数组的长度是有限的,当插入的Entry越来越多时,不同的Key通过哈希函数获得的下标有可能是相同的,这种情况,就叫作哈希冲突。

 

如何解决Hash冲突?

开放寻址法

    开放寻址法的原理是当一个Key通过哈希函数获得对应的数组下标已被占用时,就寻找下一个空档位置。

 

在Java中,ThreadLocal所使用的就是开放寻址法。

链表法

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

 

时间复杂度

写操作: O(1) + O(m) = O(m) m为单链元素个数

读操作:O(1) + O(m) m为单链元素个数

Hash冲突写单链表:O(m)

Hash扩容:O(n) n是数组元素个数 rehash

Hash冲突读单链表:O(m) m为单链元素个数

优缺点

优点:读写快

缺点:哈希表中的元素是没有被排序的、Hash冲突、扩容 重新计算

应用

JDK1.7中HashMap使用一个table数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置,如果hashcode相同,或者hashcode取模后的结果相同,那么这些key会被定位到Entry数组的同一个格子里,这些key会形成一个链表,在极端情况下比如说所有key的hashcode都相同,将会导致这个链表会很长,那么put/get操作需要遍历整个链表,那么最差情况下时间复杂度变为O(n)。

扩容死链

针对JDK1.7中的这个性能缺陷,JDK1.8中的table数组中可能存放的是链表结构,也可能存放的是红黑树结构,如果链表中节点数量不超过8个则使用链表存储,超过8个会调用treeifyBin函数,将链表转换为红黑树。那么即使所有key的hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(logn)的开销。

五.递归和二分查找

递归

本质

递归,去的过程叫"递",回来的过程叫”归“

递是调用,归是结束后回来

是一种循环,而且在循环中执行的就是调用自己

递归调用将每次返回的结果存在栈帧中

递归三要素

   递归结束条件:既然是循环就必须要有结束,不结束就会OOM了

   函数的功能:这个函数要干什么,打印,计算....

   函数的等价关系式:递归公式,一般是每次执行之间,或者与个数之间的逻辑关系

二分查找

   二分查找(Binary Search)算法,也叫折半查找算法.当我们要从一个序列中查找一个元素的时候,二分查找是一种非常快速的查找算法二分查找是针对有序数据集合的查找算法,如果是无序数据集合就遍历查找。

二分查找之所以快速,是因为它在匹配不成功的时候,每次都能排除剩余元素中一半的元素。因此可能包含目标元素的有效范围就收缩得很快,而不像顺序查找那样,每次仅能排除一个元素。

六.树

树的种类较多如下图:

 

二叉树

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

满二叉树

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

 

完全二叉树

如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。

 

二叉树的存储

  二叉树属于逻辑结构,可以使用链表和数组进行存储。

链式存储

 

数组存储

  使用数组存储时,会按照层级顺序把二叉树的节点放到数组中对应的位置上。如果某一个节点的左孩子或右孩子空缺,则数组的相应位置也空出来。

 

寻址方式:

     一个父节点的下标是n,那么它的左孩子节点下标就是2×n+1、右孩子节点下标就是2*(n+1)对于一个稀疏的二叉树(孩子不满)来说,用数组表示法是非常浪费空间的所以二叉树一般用链表存储实现。(二叉堆除外)。

二叉树的遍历

   二叉树,是典型的非线性数据结构,遍历时需要把非线性关联的节点转化成一个线性的序列,以不同的方式来遍历,遍历出的序列顺序也不同。

二叉树的遍历包括:

1. 深度优先遍历

所谓深度优先,顾名思义,就是偏向于纵深,“一头扎到底”的访问方式。它包括: 

前序遍历

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

 

步骤如下:

1、首先输出的是根节点1

2、由于根节点1存在左孩子,输出左孩子节点2

3、由于节点2也存在左孩子,输出左孩子节点4

4、节点4既没有左孩子,也没有右孩子,那么回到节点2,输出节点2的右孩子节点5

5、节点5既没有左孩子,也没有右孩子,那么回到节点1,输出节点1的右孩子节点3

6、节点3没有左孩子,但是有右孩子,因此输出节点3的右孩子节点6

到此为止,所有的节点都遍历输出完毕

 

中序遍历

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

 

步骤如下:

1、首先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深入访问下去,一直找到不

再有左孩子 的节点,并输出该节点。显然,第一个没有左孩子的节点是节点4

2、依照中序遍历的次序,接下来输出节点4的父节点2

3、再输出节点2的右孩子节点5

4、以节点2为根的左子树已经输出完毕,这时再输出整个二叉树的根节点1

5、由于节点3没有左孩子,所以直接输出根节点1的右孩子节点3

6、最后输出节点3的右孩子节点6

到此为止,所有的节点都遍历输出完毕

后序遍历

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

 

步骤如下:

1、首先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深入访问下去,一直找到不再有左孩子 的节点,并输出该节点。显然,第一个没有左孩子的节点是节点4。

2、输出右节点5

3、输出节点4的父节点2

4、以节点2为根的左子树已经输出完毕,这时再输出整个二叉树的右子树

5、访问根节点的右孩子,如果这个右孩子拥有左孩子,则继续深入访问下去,一直找到不再有左

孩子 的节点,如果没有左孩子则找右孩子,并输出该节点6

6、输出节点6的父节点3

到此为止,所有的节点都遍历输出完毕

2. 广度优先遍历

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

 

时间复杂度

二叉查找树的插入和查找时间复杂度为:O(logn)

极端情况下二叉查找树退化成链表,时间复杂度为O(n),所以需要平衡二叉查找树。

应用

非线性数据:菜单,组织结构、家谱等等。

线性数据:二叉查找树。

二叉查找树是有序的,我们只需要中序遍历,就可以在 O(n) 的时间复杂度内,输出有序的数据序列。

二叉查找树的性能非常稳定,扩容很方便(链表实现)

红黑树

平衡二叉查找树

 

这种二叉查找树就退化成了链表,由于树的深度变得多了,查找的效率也会大幅下降

所以需要对这种二叉树进行自平衡,红黑树就是一种自平衡的二叉查找树。

红黑树(Red Black Tree

除了二叉查找树(BST)的特征外,还有以下特征:

(1)每个节点要么是黑色,要么是红色

(2) 根节点是黑色

(3) 每个叶子节点都是黑色的空结点(NIL结点)(为了简单期间,一般会省略该节点)

(4) 如果一个节点是红色的,则它的子节点必须是黑色的(父子不能同为红)

(5) 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点(平衡的关键)

(6) 新插入节点默认为红色,插入后需要校验红黑树是否符合规则,不符合则需要进行平衡

一颗红黑树如下:

 

    在对红黑树进行添加或者删除操作时可能会破坏这些特点,所以红黑树采取了很多方式来维护这些特点,从而维持平衡。主要包括:左旋转、右旋转和颜色反转。

左旋(RotateLeft

逆时针旋转红黑树的两个结点,使得父结点被自己的右孩子取代,而自己成为自己的左孩子。

 

上图所示过程如下:

    1. 以X为基点逆时针旋转

    2. X的父节点被x原来的右孩子Y取代

    3. c保持不变

    4. Y节点原来的左孩子c变成X的右孩子

右旋(RotateRight

顺时针旋转红黑树的两个结点,使得父结点被自己的左孩子取代,而自己成为自己的右孩子。

 

上图所示过程如下:

1. 以X为基点顺时针旋转

2. X的父节点被x原来的左孩子Y取代

3. b保持不变

4. Y节点原来的右孩子c变成X的左孩子

颜色反转

就是当前节点与父节点、叔叔节点同为红色,这种情况违反了红黑树的规则,需要将红色向祖辈上传,父节点和叔叔节点红色变为黑色,爷爷节点从黑色变为红色(爷爷节点必为黑色,因为此前是符合红黑树规则的)。这样每条叶子结点到根节点的黑色节点数量并未发生变化,因此都其他树结构不产生影响。

多路树

这块面试比较常问,另开一篇在另外一篇博客:

https://blog.csdn.net/illovesoftware/article/details/116207829

七.图

图的概念

图(Graph),是一种复杂的非线性表结构。

图中的元素我们就叫做顶点(vertex)。

图中的一个顶点可以与任意其他顶点建立连接关系。我们把这种建立的关系叫做边(edge)。

跟顶点相连接的边的条数叫做度(degree)。

 

    图这种结构有很广泛的应用,比如社交网络,电子地图,多对多的关系就可以用图来表示。边有方向的图叫做有向图,比如A点到B点的直线距离,微信的添加好友是双向的

边无方向的图叫无向图,比如网络拓扑图

带权图(weighted graph)。在带权图中,每条边都有一个权重(weight),我们可以通过这个权重来表示 一些可度量的值

 

图的存储

图最直观的一种存储方法就是,邻接矩阵(Adjacency Matrix)。

邻接矩阵的底层是一个二维数组

 

 

无向图:如果顶点 i 与顶点 j 之间有边,我们就将 A[i][j]和 A[j][i]标记为 1

 

 

有向图:

如果顶点 i 到顶点 j 之间,有一条箭头从顶点 i 指向顶点 j 的边,那我们就将 A[i][j]标记为 1。同理,如果有一条箭头从顶点 j 指向顶点 i 的边,我们就将 A[j][i]标记为 1.

 

 

带权图

数组中就存储相应的权重

 

图的遍历

遍历是指从某个节点出发,按照一定的的搜索路线,依次访问对数据结构中的全部节点,且每个节点仅访问一次

前面已经讲过了二叉树的节点遍历

类似的,图的遍历是指,从给定图中任意指定的顶点(称为初始点)出发,按照某种搜索方法沿着图的边访问图中的所有顶点,使每个顶点仅被访问一次,这个过程称为图的遍历。遍历过程中得到的顶点序列称为图遍历序列

图的遍历过程中,根据搜索方法的不同,又可以划分为两种搜索策略:

深度优先搜索,广度优先搜索

深度优先搜索(DFS,Depth First Search

深度优先搜索,从起点出发,从规定的方向中选择其中一个不断地向前走,直到无法继续为止,然后尝

试另外一种方向,直到最后走到终点。就像走迷宫一样,尽量往深处走。

DFS 解决的是连通性的问题,即,给定两个点,一个是起始点,一个是终点,判断是不是有一条路径能

从起点连接到终点。起点和终点,也可以指的是某种起始状态和最终的状态。问题的要求并不在乎路径

是长还是短,只在乎有还是没有。

广度优先搜索(BFS,Breadth First Search

直观地讲,它其实就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。

八.算法思维

贪心算法

概念

贪婪算法(Greedy)的定义:是一种在每一步选中都采取在当前状态下最好或最优的选择,从而希望导致结果是全局最好或最优的算法。

贪婪算法:当下做局部最优判断,不能回退(能回退的是回溯,最优+回退是动态规划)

由于贪心算法的高效性以及所求得答案比较接近最优结果,贪心算法可以作为辅助算法或解决一些要求结果不特别精确的问题。

时间复杂度

在不考虑排序的前提下,贪心算法只需要一次循环,所以时间复杂度是O(n)

优缺点

优点:性能高,能用贪心算法解决的往往是最优解

缺点:在实际情况下能用的不多,用贪心算法解的往往不是最好的

适用场景

针对一组数据,我们定义了限制值和期望值,希望从中选出几个数据,在满足限制值的情况下,期望值最大。

每次选择当前情况下,在对限制值同等贡献量的情况下,对期望值贡献最大的数据(局部最优而全局最优)

大部分能用贪心算法解决的问题,贪心算法的正确性都是显而易见的,也不需要严格的数学推导证明在实际情况下,用贪心算法解决问题的思路,并不总能给出最优解

找一篇博客分析了一下这篇博文还挺好:

https://blog.csdn.net/effective_coder/article/details/8736718

分治算法

概念

分治算法(divide and conquer)的核心思想其实就是四个字,分而治之 ,也就是将原问题划分成 n个规模较小,并且结构与原问题相似的子问题,递归地解决这些子问题,然后再合并其结果,就得到原问题的解。

关于分治和递归的区别

分治算法是一种处理问题的思想,递归是一种编程技巧。

分治算法的递归实现中,每一层递归都会涉及这样三个操作:

分解:将原问题分解成一系列子问题

解决:递归地求解各个子问题,若子问题足够小,则直接求解

合并:将子问题的结果合并成原问题

时间复杂度:根据拆分情况可以是O(n)或O(logn)

优势:将复杂的问题拆分成简单的子问题,解决更容易,另外根据拆分规则,性能有可能提高。

劣势:子问题必须要一样,用相同的方式解决

适用场景

(1)分治算法能解决的问题,一般需要满足下面这几个条件:

(2)原问题与分解成的小问题具有相同的模式;

(3)原问题分解成的子问题可以独立求解,子问题之间没有相关性,这一点是分治算法跟动态规划的明显区别

(4)具有分解终止条件,也就是说,当问题足够小时,可以直接求解;

(5)可以将子问题合并成原问题,而这个合并操作的复杂度不能太高,否则就起不到减小算法总体复杂度的效果了。

找一篇博客重温了一下还挺好: https://www.pianshen.com/article/869692458/

 

 

回溯算法

概念

回溯算法实际上一个类似枚举的深度优先搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回(也就是递归返回),尝试别的路径。回溯的处理思想,有点类似枚举(列出所有的情况)搜索。我们枚举所有的解,找到满足期望的解。为了有规律地枚举所有可能的解,避免遗漏和重复,我们把问题求解的过程分为多个阶段。每个阶段,我们都会面对一个岔路口,我们先随意选一条路走,当发现这条路走不通的时候(不符合期望的解),就回退到上一个岔路口,另选一种走法继续走。

人生如果能够回退?那么你想回退到哪个阶段呢?

时间复杂度

N皇后问题的时间复杂度为: 实际为

优缺点

优点:

回溯算法的思想非常简单,大部分情况下,都是用来解决广义的搜索问题,也就是,从一组可能的解中,选择出一个满足要求的解。回溯算法非常适合用递归来实现,在实现的过程中,剪枝操作是提高回溯效率的一种技巧。利用剪枝,我们并不需要穷举搜索所有的情况,从而提高搜索效率。

劣势:

效率相对于低(动态规划)

适用场景

回溯算法是个“万金油”。基本上能用的动态规划、贪心解决的问题,我们都可以用回溯算法解决。回溯算法相当于穷举搜索。穷举所有的情况,然后对比得到最优解。不过,回溯算法的时间复杂度非常高,是指数级别的,只能用来解决小规模数据的问题。对于大规模数据的问题,用回溯算法解决的执行效率就很低了。

常见题目:八皇后问题、全排列、0-1 背包、正则表达式匹配等。

找了篇博客温故了下还不错:

http://c.biancheng.net/view/3400.html

动态规划

概念

动态规划(Dynamic Programming),是一种分阶段求解的方法。

动态规划算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。

     首先是拆分问题,我的理解就是根据问题的可能性把问题划分成一步一步这样就可以通过递推或者递归来实现. 关键就是这个步骤,动态规划有一类问题就是从后往前推到,有时候我们很容易知道:如果只有一种情况时,最佳的选择应该怎么做.然后根据这个最佳选择往前一步推导,得到前一步的最佳选择 然后就是定义问题状态和状态之间的关系,我的理解是前面拆分的步骤之间的关系,用一种量化的形式表现出来,类似于高中学的推导公式,因为这种式子很容易用程序写出来,也可以说对程序比较亲和(也就是最后所说的状态转移方程式) 我们再来看定义的下面的两段,我的理解是比如我们找到最优解,我们应该讲最优解保存下来,为了往前推导时能够使用前一步的最优解,在这个过程中难免有一些相比于最优解差的解,此时我们应该放弃,只保存最优解,这样我们每一次都把最优解保存了下来,大大降低了时间复杂度

动态规划中有三个重要概念:

最优子结构

边界

状态转移公式(递推方程)dp方程

经典问题

再谈斐波那契数列

找了篇博客漫画版真是生动易理解: https://www.sohu.com/a/149075950_684445

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值