谈谈数据结构

谈谈数据结构

1、静态数组Array
  Array类 - 线性结构,支持随机访问,通过已知的index获取对应的元素,在内存中是连续分布的,需在第一次创建时指定长度。优点:访问快。缺点:无法自动实现容量伸缩问题。System.arraycopy()方法实现数组间指定位置和数量的复制。Array.copyOf()支持全量复制,底层实现就是System.arraycopy(),复制的是对象所引用的地址,新旧数组中的对象用的指是同一个地址。
2、动态数组ArrayList
  ArrayList类 - 线性结构,支持随机访问,继承抽象类AbstractList和实现List接口,该类底层是在Array的基础上封装了一层,实现了自动容量伸缩问题。默认的初始容量是10,在扩容方面是先得到数组的旧容量,然后进行oldCapacity + (oldCapacity >> 1),将oldCapacity 右移一位,其效果相当于oldCapacity /2,我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍。增加容量后通过Array.copgOf()将原数组中的内容复制到新的数组中去,然后将原数组的引用指向新的数组引用,扩容的过程是很影响性能的,表面上看复杂度是O(n)操作,但均摊到每一次操作来算的话是O(1)操作,因为增加一定元素之后才会触发扩容操作。
3、动态数组Stack栈
  Stack类 - 线性结构,继承Vector类和实现List接口,Vecotr类继承AbstractList类,在容量伸缩,随机访问和ArrayList一样,不同的是remove一个元素只能从最后一个元素一个一个这样的删除,遵循先入后出原则。这样的优点是在删除最后一个元素时,不需要重新复制旧数组中的元素到新的数组中去,性能上有优势。比如递归的调用,把每个需要执行的指令放入栈中,当找到最后那个临界点时,再出栈,一个个去执行。正常的代码撤销也是先入栈,后出栈。java.util.Stack中peek()查看栈中最后一个元素,pop()出栈,删除最后一个元素,并返回该元素,push()入栈,往最后面追加一个元素。这三个操作都有加锁
4、静态数组ArrayBlockingQueue队列
  ArrayBlockingQueue是一个由数组支持的有界阻塞队列,先入先出原则,必须指定最大容量,有提供阻塞的方法。由于底层是数组的实现,还是线性结构,但没有做动态扩容。当队列中第一个元素被取出时,就将takeIndex++,count–如果takeIndex == items.length,到最大长度时,将takeIndex=0,从头开始取。往队列末尾添加元素,++putIndex,count++,如果++putIndex==items.length,putIndex=0。循环队列那种,当count == items.length,队列就满了。可以自己实现动态扩容队列,如果不循环的话,每次在取出第一个的时候,将整个数组往前挪一个位置,快满的时候再扩容,这样能满足扩容效果,但是每次将整个数组往前挪一个位置,操作的复杂度是O(n),很消耗性能。可以自己实现循环且能自己扩容的队列,底层是数组,只是对外提供的方法不一样而已。
5、链表
  最简单的动态数据结构,更深入的理解引用(或者指针),优点:真正的动态,不需要处理固定容量的问题。缺点:丧失了随机访问的能力。数组和链表对比:数组最好用于索引有语意的情况,最大的优点:支持快速查询。链表不适合用于索引有语意的情况。最大的优点:动态。对于链表的操作,增删改查都是O(n)的复杂度操作。如果只对链表头进行操作的话,和数组一样是O(1)的复杂度。对链表的操作,最好是使用递归去遍历。
6、二分搜索树
  树结构的特点是从root开始,所有的左子树都比它的父节点小,所有的右子树都比它的父节点大,叶子节点的左子树和右子树都为空,里面存放的数据是可以比较大小的,通过对象继承Comparable接口,实现compareTo方法。通过这样的规律来搜索数据是很快的,不向线性结构,每次都需要从头开始,一个个比较,然后找到自己需要的。打个比方:一个公司有很多部门,部门下还分很多的小组,如果要找某个小组下的某个人,只需要先找到他对应的部门,再从对应的部门下找对应的小组,再从对应的小组下找对应的人即可,就像树有很多的分支一样。缺点:不能像数组那样通过索引查询对应的值,不能存放任意的值,需存放可以比较大小的值。遍历的方式有前序遍历(),中序遍历,后序遍历。删除某个节点,如果是叶子节点,直接删除就好,如果还有左子树和右子树,先找到要删除的节点,再找到该节点的右子树后面附近最小的节点,将它替换要删除的节点,或者找到该节点的左子树后面附近最大的节点。增删查的复杂度和二分搜索树的深度相关,即每层的数量为2的h-1次方,总数量为2的h次方-1,n和h的关系为log2n=h,即二分搜索树的复杂度为O(logn)。需要注意的是,添加到二分搜索树中的数据最好的无序进来的,这样可以分布在不同的节点,查询最快。如果是从小到大,或者从大到小这样的顺序进来的话,那它和链表的结构无异,这是最差的一种情况。
7、集合Set
  分有序集合和无序集合,有序集合底层实现是二分搜索树,无序集合底层实现是哈希表,集合通常指的是无重复元素的集合。比如TreeSet,LinkedSet(基于链表的无序集合,性能比较差,O(n)的复杂度)。也有多重集合,对于有去重操作则选择用集合。
8、映射Map
  分有序映射和无序映射,有序映射底层实现是搜索树,无序映射底层实现是哈希表,映射通常指的是key对应一个value,比如TreeMap,LinkedListMap(基于链表的无序映射,性能比较差,O(n)的复杂度)。也有多重映射
9、堆
  满二叉树:除了叶子节点,其它节点的左子树和右子树节点都不为空。二叉堆:是一颗完全二叉树,不是满二叉树,缺少的节点一定在右侧,如果一层容纳不下的话,最下面那层从左到右排列。是将元素从左到右一层一层的放。堆中每个节点的值总是不大于其父节点的值。根节点的值为最大堆(同理可以定义最小堆,即堆中每个节点的值总是不小于其父节点的值)。按照这样有顺序一层一层,从左到右的顺序放入元素,就可以给每个节点定义一个index,和数组中的index一样,只不过每个节点的left child (i) = 2i + 1 即父节点的index2+1,right child(i) = 2i +2 parent(i) = (i-1)/2 即子节点的index-1,再除以2。最大堆插入数据,底层用动态数组实现,先在最后一个位置插入数据,然后再与父节点比较大小,如果比父节点大就上浮(sift up)。最大堆中的根节点就是最大的,取出最大堆后,将数组的最后一个元素放到最前面,作为根节点,然后将这个元素的左右子节点做比较,取最大的交换位置(sift down)。replace:将最大堆的值取出,用新的元素去替换。heapify:先找到最后一个非叶子节点,即数组的最后一个元素的父节点(index-1)/2,然后从这节点开始依次向前遍历做sift up操作。将n个元素逐个插入到一个空堆中,算法复杂度是O(nlogn),heapify的过程,算法复杂度为O(n)。
10、优先队列PriorityQueue
  底层是通过最小堆实现的,完全二叉树的根节点是最小的元素。通过这样的队列来实现100W取前100的值,先取前100个元素组成数组,用heapify的办法将这100个元素放入优先队列中,优先队列的大小为100,后面每次都与优先队列的第一个值比较,如果比它大就丢掉,比它小就替换第一个,这样一个个比较,得到最后的那100个,就是优先值最高的。关键点在如何定义优先级
  最小堆:根节点的值最小,每个子节点都比父子节点大,这样一层一层的放进完全二叉树中,关键是如何定义优先级的大小,如果优先级越大,值越小,那最前面那个是优先级最高的。反之,优先级越小,值越小,那最前面那个是优先级最低的。如果优先级对低的在前面,那么在将最小堆中每个元素按照优先级从高到低这样排列的话,可以取根节点的值放入栈中,这样一次的放入,由于栈是先入后出,这样就将优先级排好了。
  优先队列里的元素要么实现Comparable接口,重写compareTo方法,要么传入一个比较器,即自定义一个类实现Comparator接口,重写compare方法。对于Java自定义的类型,比如String,需要按照自己定义的比较方式比较大小的话,就可以传入一个自定义比较器进去。或者使用匿名内部类,只使用一次。Java8可以用纳姆达表达式代替匿名内部类。
11、线段树(区间树)
  可以看作是一个满的二叉树结构。如果一段区间的元素有n个,那么给到4n的静态数组空间,也许会浪费一些空间,最多浪费2n的空间,用空间换时间。主要是区间的计算,通过0到data.length-1作为startIndex和endIndex,左子树的index范围为leftIndex = startIndex + (endIndex - 1)/2,右子树的Index范围为rightIndex = startIndex + (endIndex - 1)/2,通过这样递归的去分配区间的范围,然后根据需求实现接口中的计算方法,将两个区间的对象变成一个对象。反之在查询的时候,先通过index递归判断在哪个区间,然后再一步步计算。使用线段树在更新和查询数据是都是O(logn)的复杂度。
12、Trie
  为字符串设计的集合或者映射。比如一个单词,有很多字母,添加的时候从根节点开始,按照将字母和Node放入TreeMap中,和链表类似,上一个节点中的node为下一个节点。递归到最后,如果该单词不存在就将最后那个节点的isWord设为true,size++。对于字符串的查找性能是最好的。
13、并查集
  主要用来解决连接的问题,网络中节点间的连接状态,网络是个抽象的概念:用户之间形成的网络。由子节点指向父节点,同一个父节点是处于一个连接中。基于深度的优化比基于size的优化要合理一些。路径压缩提升性能,底层是基于数组实现的。
14、平衡二叉树和AVL
  对于任意节点的左子树和右子树高度相差不能超过1,平衡二叉树的高度和节点数量之间的关系也是O(logn)的,标注节点的高度(左右子树取最大的高度+1),计算平衡因子(左右子树的高度相减)。当y节点左子树的高度减去其右子树的高度大于1时,出现不平衡了,需要做平衡调整。插入的元素在不平衡的节点的左侧的左侧(LL),进行右旋转。Node x = y.left;拿到其左子树,Node T3 = x.right;获取该左子树的右子树,x.right = y; y.left =T3,即将y的左子树的右子树改成y,将y的左子树改成x的右子树,顺时针旋转。然后再更新y的gaod,x的高度。同理,插入的元素在不平衡的节点的右侧的右侧(RR),进行左旋转。Node x = y.rigth; Node T2 = x.left; 向左旋转过程 x.left = y; y.right =T2,然后再更新高度。还有就是LR,节点的左子树高度大于右子树的高度,节点左子树的左子树的高度小于节点左子树的右子树的高度,需要先左旋转,再右旋转。同理对于RL,先右旋转再左旋转。
15、红黑树
  1、每个节点或者是红色的,或者是黑色的
  2、根节点是黑色的
  3、每一个叶子节点(最后的空节点)是黑色的
  4、如果一个节点是红色的,那么它的孩子节点都是黑色的
  5、从任意一个节点到叶子节点,经过的黑色节点都是一样的
  红黑树和2-3树类似,2-3树:满足二分搜索树的基本性质,节点可以存放一个元素或者两个元素,如果存放两个元素,子节点可以存比b小的,或者在b和c之间的,或者比从大的,这样就出现有些节点有2个孩子,有些有3个孩子。2-3树是一颗绝对平衡的树.2-3树不能直接在叶子节点为空的情况下插入,会先进行融合,融合的元素达到3个的时候,进行分裂。红黑树在增删改的性能由于AVL,查询的性能比AVL稍微差一些,原因在于红黑树不是完全平衡的二叉树,所以会出现左右子节点的高度差大于1的。但是会保持对于黑节点的绝对平衡性。
 对于完全随机数,普通的二叉树更好用(极端情况退化成链表,高度不平衡)。
 对于查询比较多,用AVL树很好用。
 红黑树牺牲了平衡性(2logn的高度)统计性能更优(综合增删改查所有的操作)
 TreeMap,TreeSet底层都是通过红黑树实现的。
16、Splay Tree (伸展树)
  局部性原理:刚被访问的内容下次高概率被再次访问
17、哈希表(链地址法)
  首先通过哈希函数将"键"转换为“索引”,然后通过索引对应对应的值,查询哈希表获取对应的值。很难保证没一个"键"通过哈希函数的转换对应不同的"索引",这样就会出现哈希冲突,最关键的点就是解决哈希冲突。
  哈希表充分体现了算法设计领域的经典思想:空间换时间(类似的思想有多存储一些东西,预处理一些东西,缓存一些东西)。
  “键”通过哈希函数得到的"索引"分布越均匀越好。
  通常通过取模的方式来设计哈希函数,比如身份证的号码,可以取后四位,但是这样只有10000的数据,hash冲突会比较大,如果取六位,将生日中的日期拿进来,hash的范围变大,冲突会减少,但是会出现分布不均匀的情况,因为日期是0到30,所以在设计哈希函数的时候结合具体业务来设计。对于一个大整数可以对一个素数进行取余,这样会分布比较均匀
  整型处理: 166 = 1
10^2 + 6*10^1 +6 *10^0
  浮点型处理:将32位或者64位的转换成整型
  字符串处理:hash(wuxin) = (((((w % M) * B + u) % M * B + x) % M * B + i) %M * B +n)%M 重点是定义这个M的范围,根据多少个不同字符来定义,还有就是取模的M,为素数,根据值的范围来取。
  转换为整型不是唯一处理,但是有三个原则:1、一致性:如果a == b ,那么hash(a) == hash(b),反之不一定成立,hash冲突就是这样来的。 2、高效性:计算高效方便(使用hash表就是为了高效的存储,如果在哈希函数的计算费时就有点得不偿失了)。3、均匀性:哈希值均匀分布(哈希表中的索引值分布开)。
  解决哈希冲突,默认是在同一个索引位置,hashcode值相同的数据在链表的结构存储,jdk8之前都是用链表结构,jdk8之后,hashcode值相同的数据在整个哈希表中不多时,用链表结构,达到一定程度之后改用红黑树存储。
  哈希表:均摊复杂度为O(1),牺牲了顺序性。有序集合,有序映射底层是平衡树,无序集合,无序映射底层是哈希表。
18、双向队列BlockingDeque
  底层通过双向链表实现的,双向链表每个节点都会保存前一个节点的引用和后一个节点的引用,同时会定义链表头和链表尾,这样就可以先入先出和先入后出的功能了。
  LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
   若某线程(线程A)要取出数据时,队列正好为空,则该线程会执行notEmpty.await()进行等待;当其它某个线程(线程B)向队列中插入了数据之后,会调用notEmpty.signal()唤醒“notEmpty上的等待线程”。此时,线程A会被唤醒从而得以继续运行。 此外,线程A在执行取操作前,会获取takeLock,在取操作执行完毕再释放takeLock。
  若某线程(线程H)要插入数据时,队列已满,则该线程会它执行notFull.await()进行等待;当其它某个线程(线程I)取出数据之后,会调用notFull.signal()唤醒“notFull上的等待线程”。此时,线程H就会被唤醒从而得以继续运行。 此外,线程H在执行插入操作前,会获取putLock,在插入操作执行完毕才释放putLock。
19、数据结构总结
  线性结构:动态数组、普通队列、栈、链表、哈希表
  树形结构:二分搜索树、AVL树、红黑树、堆、线段树、Trie、并查集
  图结构:邻接表、邻接矩阵

猜你喜欢

转载自blog.csdn.net/qq_38019655/article/details/83214785