hash,hashtable,map,tree,list,linkedlist

94 阅读16分钟

1.hash

Hash也称散列、哈希,对应的英文都是Hash。基本原理就是把任意长度的输入,通过Hash算法变成固定长度的输出。

直观解释起来,就是对一串数据m进行杂糅,输出另一段固定长度的数据h,作为这段数据的特征(指纹)。也就是说,无论数据块m有多大,其输出值h为固定长度。

哈希算法,是一种广义的算法,或者说是一种思想,它没有一个固定的公式,只要满足上面定义的算法,都可以称作Hash算法。

一个优秀的Hash算法所要具备的特点有:

  • 抗碰撞能力:对于任意两个不同的数据块,其hash值相同的可能性极小;对于一个给定的数据块,找到和它hash值相同的数据块极为困难。

由于Hash原理是将输入空间的值映射到Hash空间内,但Hash值的空间远远小于输入的空间。根据鸽巢原理,一定会存在不同输入被映射成相同输出的过程,这种情况称为“散列碰撞(collision)”。

  • 抗篡改能力:对于一个数据块,哪怕只改动其一个比特位,其hash值的改动也会非常大,而完全相同的数据可以得到完全相同的Hash值。
  • 不可以反向推导出原始的数据
  • 优秀的Hash算法执行效率要高效率,长文本也能快速计算出Hash值。

String类型的hashCode

public int hashCode() {

    // The hash or hashIsZero fields are subject to a benign data race,

    // making it crucial to ensure that any observable result of the

    // calculation in this method stays correct under any possible read of

    // these fields. Necessary restrictions to allow this to be correct

    // without explicit memory fences or similar concurrency primitives is

    // that we can ever only write to one of these two fields for a given

    // String instance, and that the computation is idempotent and derived

    // from immutable state

    int h = hash;

    if (h == 0 && !hashIsZero) {

        h = isLatin1() ? StringLatin1.hashCode(value)

                       : StringUTF16.hashCode(value);

        if (h == 0) {

            hashIsZero = true;

        } else {

            hash = h;

        }

    }

    return h;

}

通常来说,hashCode()可以看作是一种弱比较,回归Hash的本质,将不同的输入映射到固定长度的输出,那么,就会出现以下几种情况:

  1. 输入相同,输出必然相同;(抗篡改能力)
  2. 输入不同,输出可能相同,也可能不同;(基本不同,即抗碰撞能力)
  3. 输出相同,输入可能相同,也可能不同;(基本相同,只有产生碰撞了在不同)
  4. 输出不同,输入必然不同;(一个输入只可能计算出一种输出)

而equals()是严格比较两个对象对应的的值是否相等的方法,所以,如果两个对象equals()为true,那么,它们的hashCode()一定要相等,解释了String类new出来的两个对象,如果值相等,即:

String q=new String("qqqq");

String w=new String("qqqq");

q和w的存储的引用不同,即q==w为false,但q.equals(w)为true,即值相等,则产生的hashCode则一定相等。

如果equals()返回true,而hashCode()不相等,那么,试想将这两个对象作为HashMap的key,它们很大可能会定位到HashMap不同的槽中,此时就会出现一个HashMap中插入了两个相等的对象,这是不允许的,这也是为什么重写了equals()方法一定要重写hashCode()方法的原因。

2. Hashtable

哈希表也叫散列表,是根据关键值Key(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值中的Key映射到表中一个位置来访问记录,以加快查找的速度。其中,映射的实现也叫做哈希函数,而存放记录的数组叫做哈希表。

public class Hashtable<K,V>

    extends Dictionary<K,V>

    implements Map<K,V>, Cloneable, java.io.Serializable {

HashTable<K,V>也是一种key-value结构,它继承自Dictionary<K,V>,实现了Map<K,V>和Cloneable以及Serializable接口。

哈希表发展史

对于查找数组里的值来说,只能从头或者尾进行遍历查找,时间复杂度为O(n)。

于是有了hash表的概念,通过输入的值能计算出key,那么把value存到key的位置,当要查找某个值的时候,计算出key就能直接返回想要查找的值。

但这样存在冲突的问题,因为将无限的数据映射到有限的位置上,早晚会产生哈希碰撞,因此就要解决不同value有相同的key的问题。

线性探测法

即如果当前位置已经有数据了,那么就查看下一个位置能否存储,不行就继续下一个。但这样插入效率比较低,因为要不停的顺延查看,并且会产生插入的数据扎堆的问题,因为扎堆,让下次顺延插入效率更低。于是有了:

二次探测法

当出现冲突时,我不是往后一位一位这样来找空位置,而是使用原来的hash值加上i的二次方来寻找,i依次从1,2,3…这样,直到找到空位置为止。但使用二次探测法的哈希表,当放置的元素超过一半时,就会出现新元素找不到位置的情况。所以又引出一个新的概念——扩容

已放置元素达到总容量的x%时,就需要扩容了,这个x%时又叫作扩容因子

由于扩容会造成空间利用率的降低,因此就有了:

链表法 出现冲突不往数组中去放了,用一个链表把同一个数组下标位置的元素连接起来,即把数据外挂在外面,但当外挂的数据多了等于又多了一个数组,时间复杂度为O(n)。于是有了:

链表树法

既然链表的效率低,当链表长的时候升级成红黑树

红黑树的查询效率为O(log n),比链表的O(n)要高不少。

一致性Hash

在Redis中使用,内容较多,参考:

zhuanlan.zhihu.com/p/343460075

开放定址法

这种方法也称再散列法,其基本思想是:当关键字key的哈希地址 p=f(key) 出现冲突时,以p为key,在通过哈希函数f(p)产生新的哈希地址p1,如果p1仍然冲突,再以p1为key,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。

再哈希法

这种方法是同时构造多个不同的哈希函数,当第一个哈希函数冲突后,我们使用第二个哈希函数,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

建立公共溢出区

这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

构造函数

// 默认构造函数。

public Hashtable() 

 

// 指定“容量大小”的构造函数

public Hashtable(int initialCapacity) 

 

// 指定“容量大小”和“加载因子”的构造函数

public Hashtable(int initialCapacity, float loadFactor) 

 

// 包含“子Map”的构造函数

public Hashtable(Map<? extends K, ? extends V> t)

从图中可以看出:

  1. Hashtable继承于Dictionary类,实现了Map接口。Map是"key-value键值对"接口,Dictionary是声明了操作"键值对"函数接口的抽象类。
  2. Hashtable是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, count, threshold, loadFactor, modCount。
  • table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
  • count是Hashtable的大小,它是Hashtable保存的键值对的数量。
  • threshold是Hashtable的阈值,用于判断是否需要调整Hashtable的容量。threshold的值="容量*加载因子"。
  • loadFactor就是加载因子。
  • modCount是用来实现fail-fast机制的

3. map

public interface Map<K, V> {

    // Query Operations



    /**

 * Returns the number of key-value mappings in this map.  If the

 * map contains more than {  @code Integer.MAX_VALUE} elements, returns

 * {  @code Integer.MAX_VALUE}.

 *

 *  @return the number of key-value mappings in this map

 */

 int size();

简介:Map接口实现的是一组Key-Value的键值对的组合。 Map中的每个成员方法由一个关键字(key)和一个值(value)构成。它包装的是一组成对的“键-值”对象的集合,而且在Map接口的集合中也不能有重复的key出现,因为每个键只能与一个成员元素相对应。Map有两种比较常用的实现:HashMap和TreeMap等。HashMap也用到了哈希码的算法,以便快速查找一个键,TreeMap则是对键按序存放,因此它便有一些扩展的方法,比如firstKey(),lastKey()等,你还可以从TreeMap中指定一个范围以取得其子Map。键和值的关联很简单,用put(Object key,Object value)方法即可将一个键与一个值对象相关联。用get(Object key)可得到与此key对象所对应的值对象。

//Hashtable实现该接口

public class Hashtable<K,V>

    extends Dictionary<K,V>

    implements Map<K,V>, Cloneable, java.io.Serializable {

    

//HashMap也实现该接口  

public class HashMap<K,V> extends AbstractMap<K,V>

    implements Map<K,V>, Cloneable, Serializable {
Map t = new HashMap();

t.put("uu", "hhh");//字符串常量是对象

System.out.println(t.get("uu"));

4. tree

为什么需要树这种数据结构

  1. 数组存储方式的分析

优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。

缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低

  1. 链式存储方式的分析

优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可,删除效率也很好)。

缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)

  1. 树存储方式的分析

能提高数据存储,读取的效率, 比如利用 二叉排序树(左节点小于根节点,右节点大于根节点),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。

接口类型

public interface Tree {



    /**

 * Enumerates all kinds of trees.

 */

部分种类

public enum Kind {

    /**

 * Used for instances of {  @link AnnotatedTypeTree}

 * representing annotated types.

 */

 ANNOTATED_TYPE(AnnotatedTypeTree.class),



    /**

 * Used for instances of {  @link AnnotationTree}

 * representing declaration annotations.

 */

 ANNOTATION(AnnotationTree.class),



    /**

 * Used for instances of {  @link AnnotationTree}

 * representing type annotations.

 */

 TYPE_ANNOTATION(AnnotationTree.class),



    /**

 * Used for instances of {  @link ArrayAccessTree}.

 */

 ARRAY_ACCESS(ArrayAccessTree.class),



    /**

 * Used for instances of {  @link ArrayTypeTree}.

 */

 ARRAY_TYPE(ArrayTypeTree.class),



    /**

 * Used for instances of {  @link AssertTree}.

 */

 ASSERT(AssertTree.class),



    /**

 * Used for instances of {  @link AssignmentTree}.

 */

 ASSIGNMENT(AssignmentTree.class),

树(Tree)是n(n≥0)个节点的有限集。n=0时称为空树。在任意一棵非空树中:

  • 有且仅有一个特定的称为根(Root)的节点r;

  • 当n>1时,其余节点可分为m(m>0)个互不相交的不为空的有限集T1、T2、……、Tm,其中每一个集本身又是一棵树,并且称为根的子树(SubTree)。

二叉树

三叉树

树的结构还可以是不规则的

特点:

  • 每个节点都只有有限个子节点或无子节点;
  • 没有父节点的节点称为根节点;
  • 每一个非根节点有且只有一个父节点;
  • 除了根节点外,每个子节点可以分为多个不相交的子树;
  • 树里面没有环路(cycle)

术语

  1. 节点的度:一个节点含有的子树的个数称为该节点的度;

  2. 树的度:一棵树中,最大的节点度称为树的度;

  3. 叶节点或终端节点:度为零的节点;

  4. 非终端节点或分支节点:度不为零的节点;

  5. 父亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;

  6. 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;

  7. 兄弟节点:具有相同父节点的节点互称为兄弟节点;

  8. 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;

  9. 深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0;

  10. 高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;

  11. 堂兄弟节点:父节点在同一层的节点互为堂兄弟;

  12. 节点的祖先:从根到该节点所经分支上的所有节点;

  13. 子孙:以某节点为根的子树中任一节点都称为该节点的子孙

树的种类

  • 无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;

  • 有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;

  • 二叉树:每个节点最多含有两个子树的树称为二叉树;

  • 完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树;

  • 满二叉树:所有叶节点都在最底层的完全二叉树;

  • 平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树;

  • 排序二叉树(二叉查找树(英语:Binary Search Tree)):也称二叉搜索树、有序二叉树;

  • 霍夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树;

  • B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多于两个子树。

树的遍历

所谓树的遍历(Traversal),就是按照某种次序访问树中的节点,且每个节点恰好访问一次。

也就是说,按照被访问的次序,可以得到由树中所有节点排成的一个序列。

前序遍历

对任一(子)树的前序遍历,将首先访问其根节点,然后再递归地对其下的各棵子树进行前序遍历。对于同一根节点下的各棵子树,遍历的次序通常是任意的;但若换成有序树,则可以按照兄弟间相应的次序对它们实施遍历。由前序遍历生成的节点序列,称作前序遍历序列。

中序遍历

就是对每一棵子树,都采取左子树-根节点-右子树的遍历顺序。

后续遍历

对称地,对任一(子)树的后序遍历将首先递归地对根节点下的各棵子树进行后序遍历,最后才访问根节点。由后序遍历生成的节点序列,称作后序遍历序列。

层次遍历

除了上述两种最常见的遍历算法,还有其它一些遍历算法,层次遍历(Traversal by level )算法就是其中的一种。在这种遍历中,各节点被访问的次序取决于它们各自的深度,其策略可以总结为“深度小的节点优先访问”。

对于同一深度的节点,访问的次序可以是随机的,通常取决于它们的存储次序,即首先访问由firstChild指定的长子,然后根据nextSibling确定后续节点的次序。当然,若是有序树,则同深度节点的访问次序将与有序树确定的次序一致。

前序(根左右),中序(左根右),后序(左右根)

红黑树

红黑树是平衡二叉查找树的一种。

BST存在的主要问题是,数在插入的时候会导致树倾斜,不同的插入顺序会导致树的高度不一样,而树的高度直接的影响了树的查找效率。理想的高度是logN,最坏的情况是所有的节点都在一条斜线上,这样的树的高度为N。

红黑树的定义如下:

  1. 任何一个节点都有颜色,黑色或者红色
  2. 根节点是黑色的
  3. 任何一个节点向下遍历到其子孙的叶子节点,所经过的黑节点个数必须相等
  4. 空节点被认为是黑色的
  5. 所有叶子都是黑色(叶子是NIL节点)。
  6. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)

RBTree的旋转操作

旋转操作(Rotate)的目的是使节点颜色符合定义,让RBTree的高度达到平衡。

Rotate分为left-rotate(左旋)和right-rotate(右旋),区分左旋和右旋的方法是:待旋转的节点从左边上升到父节点就是右旋,待旋转的节点从右边上升到父节点就是左旋。

红黑树的修改操作和BST的操作类似,只是在修改后为避免出现倾斜进行修复操作。

插入

在插入新节点时,这个节点应该是红色,如果插入的节点是黑色,那么这个节点所在路径比其他路径多出一个黑色节点,这个调整起来会比较麻烦。

如果插入的节点是红色,此时所有路径上的黑色节点数量不变,仅可能会出现两个连续的红色节点的情况。这种情况下,通过变色和旋转进行调整即可,

删除

具体参考

zhuanlan.zhihu.com/p/91960960

数据结构可视化

www.cs.usfca.edu/~galles/vis…

AVL Tree为例子

暂时无法在文档外展示此内容

5. List

接口

public interface List<E> extends Collection<E> {

    // Query Operations



    /**

 * Returns the number of elements in this list.  If this list contains

 * more than {  @code Integer.MAX_VALUE} elements, returns

 * {  @code Integer.MAX_VALUE}.

 *

 *  @return the number of elements in this list

 */

 int size();

“表”结构的定义是:在一维空间下元素按照某种逻辑结构进行线性连接排列的数据结构(一对一)。java中集合定义中所包括的数组表(ArrayList)、链表(LinkedList)、各种队列(Queue/Deque)、栈(Stack)等都满足这样的定义。

List列表 —— 有序、值可重复

6. linkedlist

LinkedList类

public class LinkedList<E>

    extends AbstractSequentialList<E>

    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

{

    transient int size = 0;



    /**

 * Pointer to first node.

 */

 transient Node<E> first;



    /**

 * Pointer to last node.

 */

 transient Node<E> last;

基本属性

  • transient int size = 0; //LinkedList中存放的元素个数
  • transient Node first; //头节点
  • transient Node last; //尾节点
  • Collection 接口:Collection接口是所有集合类的根节点,Collection表示一种规则,所有实现了Collection接口的类遵循这种规则

LinkedList是通过双向链表去实现的,既然是链表实现那么它的随机访问效率比ArrayList要低,顺序访问的效率要比较的高。每个节点都有一个前驱(之前前面节点的指针)和一个后继(指向后面节点的指针)

构造方法

LinkedList() 

LinkedList(Collection<? extends E> c)

LinkedList没有长度的概念(指针相连,不像数组存储一样需要初始化数组的大小),所以不存在容量不足的问题,因此不需要提供初始化大小的构造方法,因此只提供了两个方法,一个是无参构造方法,初始一个LinkedList对象,和将指定的集合元素转化为LinkedList构造方法。

LinkedList list1=new LinkedList();

list1.add(0,"q");

list1.add(1,"w");

System.out.println(list1.get(1));

无法复制加载中的内容