前言html
对于绝大多少程序员来讲,数据结构与算法绝对是一门很是重要但又很是难以掌握的学科。最近本身系统学习了一套数据结构与算法的课程,也开始到Leetcode上刷题了。这里对课程中讲到的一些数据结构与算法基础作了一些回顾和总结,从宏观上先来了解整个知识框架。java
数据结构与算法总览图程序员
一、数组(Array)算法
数组的底层硬件实现是,有一个叫内存控制器的结构,为数组分配一个段连续的内存空间,这些空间中存储着数组中对应的值(值为基本数据类型)或者地址(值为引用类型)。当根据index访问数组中的某个元素时,内存控制器直接定位到该index所在的地址,不管是第一个元素、中间元素仍是最后一个元素,都能一次性定位到,时间复杂度为O(1)。api
Java中ArrayList是对数组的一个典型使用,其内部维护着一个数组,ArrayList的增、删、查等,都是对其中数组进行操做。因此根据index进行查找时比较快,时间复杂度为O(1);但增长和删除元素时须要扩容或者移动数组元素的位置等操做,其中扩容时还会开辟更大容量的数组,将原数组的值复制到新数组中,并将新数组复制给原数组,因此此时时间复杂度和空间复杂度为O(n)。对于频繁查找数据时,使用ArrayList效率比较高。数组
ArrayList源码:http://developer.classpath.org/doc/java/util/ArrayList-source.html缓存
二、链表(Linked List)安全
能够经过使用双向链表或者设置头尾指针的方式,让操做链表更加方便。网络
Java中LinkedList是对链表的一个典型使用,其内部维护着一个双向链表,对数据的增,删、查、改操做实际上都是对链表的操做。增、删、改非首位节点自己操做时间复杂度为O(1),可是要查找到对应操做的位置,实际上也要通过遍历查找,而链表的时间复杂度为O(n)。数据结构
LinkedList源码:http://developer.classpath.org/doc/java/util/LinkedList-source.html
参考阅读:http://www.javashuo.com/article/p-cctdnsmh-y.html
三、跳表(Skip List)
跳表是在一个有序链表的基础上升维,添加多级索引,以空间换时间,其空间复杂度为O(n),用于存储索引节点。其有序性对标的是平衡二叉树,二叉搜索树等数据结构。
数组、链表、跳表对增、删、查时间复杂度比较:
数组 | 链表 | 跳表 | |
preppend | O(n) | O(1) | O(logn) |
append | O(1) | O(1) | O(logn) |
lookup | O(1) | O(n) | O(logn) |
insert | O(n) | O(1) | O(logn) |
delete | O(n) | O(1) | O(logn) |
四、栈(Stack)
Java中虽然提供了Stack类(内部维护的实际上也是一个数组)用于实现栈,但官方文档 https://www.apiref.com/java11-zh/java.base/java/util/Stack.html中明确说明,应该优先使用Deque来实现:
Deque<Integer> stack = new ArrayDeque<Integer>();
Deque接口及其实现,提供了一套更完整和一致的LIFO(Last in first out ,后进先出)堆栈操做,这里列举几个用于栈的方法:
public E peek():检索但不移除此双端队列表示的队列的头部(换句话说,此双端队列的第一个元素),若是此双端队列为空,则返回 null
。
public E pop:今后双端队列表示的堆栈中弹出一个元素。
public void push(E e):在不违反容量限制的状况下执行此操做, 能够添加元素到此双端队列表示的堆栈(换句话说,在此双端队列的头部),若是当前没有可用空间则抛出 IllegalStateException
。
ArrayDeque实现类中,实际上也是维护的一个数组,下面会对该类作进一步介绍。
五、队列(Queue)
Java中提供了实现接口Queue,源码为:http://fuseyism.com/classpath/doc/java/util/Queue-source.html ;参考文档:https://www.apiref.com/java11-zh/java.base/java/util/Queue.html。Java还提供了不少实现类,好比ArrayDeque、LinkedList、PriorityQueue等,可使用以下方式来使用Queue接口:
Queue<String> queue = new LinkedList<String>();
Queue接口针对入队、出队、查看队尾操做提供了两套API:
第一套为,boolean add(E e) 、E element()、E remove(),在超过容量限制或者获得元素为null时,会报异常。
第二套为,boolean offer(E e)、E peek()、E poll(),不会报异常,而是返回true/false/null,通常工程中使用这一套api。
实现类LinkedList中实际维护的是一个双向链表,前面已经介绍过了。
六、双端队列(Deque)
Deque是Double end queue的缩写,参考文档:https://www.apiref.com/java11-zh/java.base/java/util/Deque.html。
Deque既提供了用于实现Stack LIFO的push、pop、peek,又提供了用于实现Queue FIFO(First In First Out:先进先出)的offer、poll、peek(add、remove、element等也有,这里仅列出推荐使用的),因此能够用于实现Stack,也能够用于实现Queue。同时,Deque还提供了全新的接口用于对应Stach和Queue的方法,如offerFirst/offerLast、peekFirst/peekLast,pollFirst/pollLast等,另外还提供了一个addAll(Collection c),批量插入数据。
前面讲Stack的时候已经介绍过了,Deque是一个接口,通常在工程中的使用方式为:
Deque<Integer> deque = new ArrayDeque<Integer>();
ArrayDeque内部维护的是一个数组。
七、优先队列(PriorityQueue)
Java中提供了PriorityQueue类来实现优先队列,是接口Queue的实现类。和Queue的FIFO不一样的是,PriorityQueue中的元素是按照必定的优先级排序的。默认状况下,其内部是经过一个小顶堆来实现排序的,也能够自定义排序的优先级。堆是一个彻底二叉树,能够用数组来表示,因此PriorityQueue内部其实是维护的一个数组。
PriorityQueue提供了对队列的基本操做:offer用于向堆中插入元素,插入后会堆会进行调整;peek用于查看但不删除数组的第一个元素,也就是堆顶的元素,是优先级最高(最大或者最小)的元素;poll用于获取并移除堆顶元素,堆会再进行调整。固然,对应的还有add/element/remove方法,这在前面Queue部分讲过了。
官方文档:https://docs.oracle.com/javase/10/docs/api/java/util/PriorityQueue.html
参考阅读:https://blog.csdn.net/u010623927/article/details/87179364
八、哈希表(Hash Table)
哈希表也叫散列表,经过键值对key-value的方式直接进行存储和访问的数据结构。它经过一个映射函数将Key映射到表中的对应位置,从而能够一次性找到对应key-value结点的位置。
Java中提供了HashTable、HashMap、ConcurrentHashMap等类来实现哈希表,这三者也常常被拿来作比较,这里简单介绍一下这三个类:
HashTable:1)内部经过数组 + 单链表实现;2)主要方法都加了Synchronized锁,线程安全,但也是由于加了锁,因此效率比其它两个差;3)Key和Value均不容许为null;
HashTable内部结构图
HashMap:1)Jdk1.7及以前,内部经过数组 + 单链表实现;Jdk1.8开始,内部经过 数组 + 单链表 + 红黑树实现 ;2)非线程安全,若是要保证线程安全,通常经过 Map m = Collections.synchronizedMap(new HashMap(...));的方式来实现,因为没有加锁,因此HashMap效率比较高;3)容许一个Key为null,Value也能够为null。
ConcurrentHashMap:分段加锁,相比HashMap它是线程安全的,相比HashTable它效率高,能够当作是对HashMap和HashTable的综合。
HashMap内部结构图
九、映射(Map)
映射中是以键值对Key-Value的形式存储元素的,其中Key不容许重复,但Value能够重复。Java中提供了Map接口来定义映射,还提供了如HashMap、ConcurrentHashMap等实现类,这两个类前面有简单介绍过。
十、集合(Set)
集合中不容许有重复的元素,添加元素时若是有重复,会覆盖掉原来的元素。Java中提供了Set接口来定义集合,也提供了HashSet实现类。HashSet类的内部实际上维护了一个HashMap,将添加的对象做为HashMap的key,Object对象做为value,以此来实现集合中的元素不重复。
1 //HashSet部分源码 2 public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { 3 ...... 4 private transient HashMap<E,Object> map; 5 private static final Object PRESENT = new Object(); 6 public HashSet() { 7 map = new HashMap<>(); 8 } 9 ...... 10 public boolean add(E e) { 11 return map.put(e, PRESENT)==null; 12 } 13 ...... 14 public boolean remove(Object o) { 15 return map.remove(o)==PRESENT; 16 } 17 ...... 18 }
十一、树(Tree)
在单链表的基础上,若是一个节点的next有一个或者多个,就构成了树结构,因此单链表是一棵特殊的树,其child只有一个。关于树有不少特定的结构,用于平时的工程中,出现得比较多得就是二叉树,而二叉树根据用途和性质又有多种类型,常见的有:
彻底二叉树:若设二叉树的深度为k,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,第k 层全部的结点都连续集中在最左边,这就是彻底二叉树。彻底二叉树能够按层存储在数组中,若是某个结点的索引为i,那么该结点若是有左右结点,那么左右结点的索引分别为2i+1,2i+2;
满二叉树:一个二叉树,若是每个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,若是一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。因此,满二叉树也是彻底二叉树。
二叉搜索树(Binary Search Tree):又称为二叉排序树、有序二叉树、排序二叉树等。其特征为:任意一个结点的左子树的值都小于/等于该结点的值,右子树的值都大于/等于根结点的值;中序遍历的结果是一个升序数列;任意一个结点的左右子树也是二叉搜索树。以下图所示:
在极端的状况下,二叉搜索树会呈一个单链表。
平衡二叉树(AVL):它或者是一颗空树,或它的左子树和右子树的深度之差(平衡因子)的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树。平衡二叉树也是一棵二叉搜索树,因为具备平衡性,因此整棵树比较平衡,不会出现一长串单链表的结构,在查找时最坏的状况也是O(logn)。为了保持平衡性,每次插入的时候都须要调整以达到平衡。
以下图所示,任意一个结点的左右子树的深度差绝对值都不超过1,且符合二叉搜索树的特色:
十二、红黑树
红黑树是一颗平衡二叉搜索树,具备平衡性和有序性,结点的颜色为红色或者黑色。这里的“平衡”和平衡二叉树的“平衡”粒度上不一样,平衡二叉数更为严格,致使在插入或者删除数据时调整树结构的频率过高了,这会致使必定的性能问题。而红黑树的平衡是任意一个结点的左右子树,较高的子树与较低子树之间的高度差不超过两倍,这样就能从必定层度上避免过于频繁调整结构。能够认为红黑树是对平衡二叉树的一种变体。
1三、图(Graph)
单链表是特殊的树,树是特殊的图。
1四、堆(Heap)
堆是一种能够迅速找到最大值或者最小值的数据结构,内部维护着一棵树(注意这里说的是树,而不是限制于二叉树,也能够是多叉)。若是该堆的根结点是最大值,则称之为大顶堆(或大根堆);若是根结点是最小值,则称为小顶堆(或小根堆)。堆的实现有不少,这里主要介绍一下二叉堆。
二叉堆,顾名思义,就是堆中的树是一棵二叉树,且是彻底二叉树(这里要注意区别于二叉搜索树),因此能够用数组表示,前面介绍的PriorityQueue就是一个堆的实现。若是是大顶堆,任何一个结点的值都 >= 其子结点的值大;若是是小顶堆,则任何一个结点的值都 <= 其子节点的值。下图展现了一个二叉大顶堆,其对应的一维数组为[110, 100, 90, 40, 80, 20, 60, 10, 30, 50, 70]:
对于大顶堆而言,通常常使用的操做是查找最大值、删除最大值和插入一个值,其时间复杂度分别为:查找最大值的时间复杂度是O(1),由于最大值就是根结点的值,位于数组的第一个位置;删除最大值,找到最大值的时间复杂度是O(1),可是删除后该堆须要从新调整,将最底层最末尾的结点移到根结点,而后根节点再与子结点点比较,并与较大的结点交换,直到该结点不小于子结点为止,因为是从最末尾的结点直接升到根结点,因此该结点的值确定是相对很小的,须要调整屡次才能再次符合堆的定义,因此时间复杂度为O(logn);插入一个结点,其作法是在数组的最后插入,也就是二叉树的最后一个层的末尾位置插入,而后再和其父结点比较,若是新结点大就和父结点交换位置,直到不大于根结点为止,因此插入新的结点可能一次到位,时间复杂度为O(1),也有可能还须要调整,最坏的时候比较和交换O(logn),即时间复杂度为O(logn)。同理,小顶堆也是如此。
堆的实现代码参考:https://shimo.im/docs/Lw86vJzOGOMpWZz2/read
1五、并查集(Disjoint Set)
并查集通常用于解决元素,组团或者配对的问题,便是否在一个集合的问题。它管理着一系列不相交的集合,主要提供以下三种基本操做:
(1)makeSet(s),建立并查集:建立一个新的并查集,其中包含s个单元素集合;
(2)unionSet(x,y),合并集合:将x元素和y元素所在的集合不相交,就将这两个集合合并;若是这两个结合相交,则不合并;
(3)find(x),查找表明:查找x元素所在集合的表明元素,该操做能够用于判断两个元素是否在同一个集合中,若是两个元素的表明相同,表示在同一个集合;不然,不在同一个集合。
若是想避免并查集过高,还能够进行路径压缩。
实现并查集的基本代码模板:
1 public class UnionFind { 2 private int count = 0; 3 private int[] parent; 4 5 //初始化并查集,用数组存储每一个元素的父节点,一开始将他们的父节点设为本身 6 public UnionFind(int n) { 7 count = n; 8 parent = new int[n]; 9 for (int i = 0; i < n; i++) { 10 parent[i] = i; 11 } 12 } 13 14 //找到元素x所在集合的表明元素 15 public int find(int x) { 16 while (x != parent[x]) { 17 x = parent[x]; 18 } 19 return x; 20 } 21 22 //合并x和y所在的集合 23 public void union(int x, int y) { 24 int rootX = find(x); 25 int rootY = find(y); 26 if (rootX == rootY) 27 return; 28 parent[rootX] = rootY; 29 count--; 30 } 31 }
这里推荐一篇写不错的文章:https://www.cnblogs.com/noKing/p/8018609.html
1六、字典树(Trie)
字典树,即Trie树,又称为前缀树、单词查找树或者键树,是一种树形结构。Trie的优势是最大限度地减小无畏的字符串比较,查询效率比hash表高。其典型应用是统计字符串(不限于字符串)出现的频次,查找具备相同前缀的字符串等,因此常常被搜索引擎用于统计单词频次,或者关键字提示,以下图所示:
Trie树具备以下特性:
(1)结点自己不存储完整单词;
(2)从根结点到某一结点,路径上通过的字符串联起来,对应的就是该结点表示的字符串;
(3)每一个结点全部的子结点路径表明的字符都不相同。
实际工程中,结点能够存储一些额外的信息,以下图就表示一棵Trie树,每一个结点存储了其对应表示的字符串,以及该字符串被统计的频次。
对于一个仅由26个小写英文字母组成的字符串造成的Trie树,其结点的内部结构为:
Trie树的核心思想是以空间换时间,由于须要额外建立一棵Trie树,它利用字符串的公共前缀来下降查询的时间的开销从而提高效率。
1七、布隆过滤器(Bloom Filter)
布隆过滤器典型应用有,垃圾邮件/评论过滤、某个网址是否被访问过等场景,它是由一个很长的二进制向量和一系列的hash函数实现的,其结构以下图所示:
一个元素A通过多个hash函数(本例中是两个)计算后获得多个hash code,在向量表中code对应的位置的值就设置为1。
其具备以下特色:
(1)存储的信息是比较少的,不会存储整个结点的信息,相比于HashMap/HashTable而言,节约了大量的空间;
(2)若是判断某个元素不存在,则必定不存在;
(3)具备必定的误判率,并且插入的元素越多,误判率越过,若是判断某个元素存在,那只能说可能存在,须要再作进一步的判断,因此称为过滤器;
因此,其优势是空间效率和查询时间都远远优于通常的算法;缺点是具备必定的误判率,且删除元素比较困难(向量表中每个位置可能对应着众多元素)。
参考阅读:https://baike.baidu.com/item/bloom%20filter/6630926?fr=aladdin
1八、LRU Cache
LRU,即Least Recently Used,最近最少使用,应用很是普遍,在Android的网络图片加载工具ImageLoader等中都具备使用。其思想为,因为空间资源有限,当缓存超过指定的Capacity时,那些最近最少使用的缓存就会被删除掉,其工做机制以下图所示:
不一样的语言中都提供了相应的类来实现LRU Cache,Java中提供的类为LinkedHashMap,内部实现思想为HashMap + 双向链表。咱们也能够经过HashMap + 双向链表本身实现一个LRU Cache。
1 //空间复杂度O(k),k表示容量 2 //小贴士:在双向链表的实现中,使用一个伪头部(dummy head)和伪尾部(dummy tail)标记界限,这样在添加节点和删除节点的时候就不须要检查相邻的节点是否存在。 3 class LRUCache { 4 HashMap<Integer, LNode> cache = new HashMap<>();//使用hashmap能够根据key一次定位到value 5 int capacity = 0;//容量 6 int size = 0; 7 //采用双链表 8 LNode head; 9 LNode tail; 10 11 public LRUCache(int capacity) { 12 this.capacity = capacity; 13 //初始化双链表 14 head = new LNode(); 15 tail = new LNode(); 16 head.next = tail; 17 tail.prev = head; 18 } 19 20 //时间复杂度:O(1) 21 public int get(int key) { 22 //先从缓存里面查,不存在返回-1;存在则将该节点移动到头部,表示最近使用过,且返回该节点的value 23 LNode lNode = cache.get(key); 24 if (lNode == null) return -1; 25 moveToHead(lNode); 26 return lNode.value; 27 } 28 29 //时间复杂度O(1) 30 public void put(int key, int value) { 31 LNode lNode = cache.get(key); 32 //若是hashmap中不存在该key 33 if (lNode == null) { 34 size++; 35 //若是已经超过容量了,须要先删除尾部节点,且从hashmap中删除掉该元素 36 if (size > capacity) { 37 cache.remove(tail.prev.key); 38 removeNode(tail.prev); 39 size--; 40 } 41 //将新的节点存入hashmap,并添加到链表的头部 42 lNode = new LNode(key, value); 43 cache.put(key, lNode); 44 addToHead(lNode); 45 } else { 46 //若是hashmap中存在该key,则修改该节点的value,且将该节点移动到头部 47 lNode.value = value; 48 removeNode(lNode); 49 addToHead(lNode); 50 } 51 } 52 53 /** 54 * 将节点移动到头部 55 */ 56 public void moveToHead(LNode lNode) { 57 removeNode(lNode); 58 addToHead(lNode); 59 } 60 61 /** 62 * 移除节点 63 */ 64 public void removeNode(LNode lNode) { 65 lNode.prev.next = lNode.next; 66 lNode.next.prev = lNode.prev; 67 lNode.next = null; 68 lNode.prev = null; 69 } 70 71 /** 72 * 在头部添加节点 73 */ 74 private void addToHead(LNode lNode) { 75 head.next.prev = lNode; 76 lNode.next = head.next; 77 head.next = lNode; 78 lNode.prev = head; 79 } 80 } 81 82 class LNode { 83 int key; 84 int value; 85 LNode prev; 86 LNode next; 87 88 public LNode() { 89 } 90 91 public LNode(int key, int value) { 92 this.key = key; 93 this.value = value; 94 } 95 }
最后
最后附上一张常见数据结构的时间和空间复杂度表