zoukankan      html  css  js  c++  java
  • 数据结构与算法总览

    总览

    一维:

    数组:数组的底层硬件实现是,有一个叫内存控制器的结构,为数组分配一个段连续的内存空间,这些空间中存储着数组中对应的值(值为基本数据类型)或者地址(值为引用类型)。当根据index访问数组中的某个元素时,内存控制器直接定位到该index所在的地址,无论是第一个元素、中间元素还是最后一个元素,都能一次性定位到,时间复杂度为O(1)。

     

            Java中ArrayList是对数组的一个典型使用,其内部维护着一个数组,ArrayList的增、删、查等,都是对其中数组进行操作。所以根据index进行查找时比较快,时间复杂度为O(1);但增加和删除元素时需要扩容或者移动数组元素的位置等操作,其中扩容时还会开辟更大容量的数组,将原数组的值复制到新数组中,并将新数组复制给原数组,所以此时时间复杂度和空间复杂度为O(n)。对于频繁查找数据时,使用ArrayList效率比较高。

            ArrayList源码:http://developer.classpath.org/doc/java/util/ArrayList-source.html

    链表:可以通过使用双向链表或者设置头尾指针的方式,让操作链表更加方便。

            LinkedList源码:http://developer.classpath.org/doc/java/util/LinkedList-source.html

           Java中LinkedList是对链表的一个典型使用,其内部维护着一个双向链表,对数据的增,删、查、改操作实际上都是对链表的操作。增、删、改非首位节点本身操作时间复杂度为O(1),但是要查找到对应操作的位置,实际上也要经过遍历查找,而链表的时间复杂度为O(n)。

           参考阅读:https://www.cnblogs.com/LiaHon/p/11107245.html

    跳表:

           跳表是在一个有序链表的基础上升维,添加多级索引,以空间换时间,其空间复杂度为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和ConcurrentHashMap内部结构图

     映射(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,且符合二叉搜索树的特点:

          红黑树:

               红黑树是一颗平衡二叉搜索树,具有平衡性和有序性,结点的颜色为红色或者黑色。这里的“平衡”和平衡二叉树的“平衡”粒度上不同,平衡二叉数更为严格,导致在插入或者删除数据时调整树结构的频率太高了,这会导致一定的性能问题。而红黑树的平衡是任意一个结点的左右子树,较高的子树与较低子树之间的高度差不超过两倍,这样就能从一定层度上避免过于频繁调整结构。可以认为红黑树是对平衡二叉树的一种变体。

        

    图(Graph)

          单链表是特殊的树,树是特殊的图。

     堆(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

    二维:

    特殊:

  • 相关阅读:
    js固定在顶部
    css垂直居中
    HTML 5离线存储
    jdbc数据库操作
    I/O输入输出流
    异常处理
    java实现冒泡排序算法
    Java 方法
    java 循环 基本类型
    Java中Arrays工具类
  • 原文地址:https://www.cnblogs.com/andy-songwei/p/14165491.html
Copyright © 2011-2022 走看看