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的本质,将不同的输入映射到固定长度的输出,那么,就会出现以下几种情况:
- 输入相同,输出必然相同;(抗篡改能力)
- 输入不同,输出可能相同,也可能不同;(基本不同,即抗碰撞能力)
- 输出相同,输入可能相同,也可能不同;(基本相同,只有产生碰撞了在不同)
- 输出不同,输入必然不同;(一个输入只可能计算出一种输出)
而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)
从图中可以看出:
- Hashtable继承于Dictionary类,实现了Map接口。Map是"key-value键值对"接口,Dictionary是声明了操作"键值对"函数接口的抽象类。
- 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
为什么需要树这种数据结构
- 数组存储方式的分析
优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低
- 链式存储方式的分析
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可,删除效率也很好)。
缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)
- 树存储方式的分析
能提高数据存储,读取的效率, 比如利用 二叉排序树(左节点小于根节点,右节点大于根节点),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。
接口类型
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层,以此类推;
-
深度:对于任意节点n,n的深度为从根到n的唯一路径长,根的深度为0;
-
高度:对于任意节点n,n的高度为从n到一片树叶的最长路径长,所有树叶的高度为0;
-
堂兄弟节点:父节点在同一层的节点互为堂兄弟;
-
节点的祖先:从根到该节点所经分支上的所有节点;
-
子孙:以某节点为根的子树中任一节点都称为该节点的子孙
树的种类
-
无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;
-
有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;
-
二叉树:每个节点最多含有两个子树的树称为二叉树;
-
完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树;
-
满二叉树:所有叶节点都在最底层的完全二叉树;
-
平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树;
-
排序二叉树(二叉查找树(英语:Binary Search Tree)):也称二叉搜索树、有序二叉树;
-
霍夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树;
-
B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多于两个子树。
树的遍历
所谓树的遍历(Traversal),就是按照某种次序访问树中的节点,且每个节点恰好访问一次。
也就是说,按照被访问的次序,可以得到由树中所有节点排成的一个序列。
前序遍历
对任一(子)树的前序遍历,将首先访问其根节点,然后再递归地对其下的各棵子树进行前序遍历。对于同一根节点下的各棵子树,遍历的次序通常是任意的;但若换成有序树,则可以按照兄弟间相应的次序对它们实施遍历。由前序遍历生成的节点序列,称作前序遍历序列。
中序遍历
就是对每一棵子树,都采取左子树-根节点-右子树的遍历顺序。
后续遍历
对称地,对任一(子)树的后序遍历将首先递归地对根节点下的各棵子树进行后序遍历,最后才访问根节点。由后序遍历生成的节点序列,称作后序遍历序列。
层次遍历
除了上述两种最常见的遍历算法,还有其它一些遍历算法,层次遍历(Traversal by level )算法就是其中的一种。在这种遍历中,各节点被访问的次序取决于它们各自的深度,其策略可以总结为“深度小的节点优先访问”。
对于同一深度的节点,访问的次序可以是随机的,通常取决于它们的存储次序,即首先访问由firstChild指定的长子,然后根据nextSibling确定后续节点的次序。当然,若是有序树,则同深度节点的访问次序将与有序树确定的次序一致。
前序(根左右),中序(左根右),后序(左右根)
红黑树
红黑树是平衡二叉查找树的一种。
BST存在的主要问题是,数在插入的时候会导致树倾斜,不同的插入顺序会导致树的高度不一样,而树的高度直接的影响了树的查找效率。理想的高度是logN,最坏的情况是所有的节点都在一条斜线上,这样的树的高度为N。
红黑树的定义如下:
- 任何一个节点都有颜色,黑色或者红色
- 根节点是黑色的
- 任何一个节点向下遍历到其子孙的叶子节点,所经过的黑节点个数必须相等
- 空节点被认为是黑色的
- 所有叶子都是黑色(叶子是NIL节点)。
- 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
RBTree的旋转操作
旋转操作(Rotate)的目的是使节点颜色符合定义,让RBTree的高度达到平衡。
Rotate分为left-rotate(左旋)和right-rotate(右旋),区分左旋和右旋的方法是:待旋转的节点从左边上升到父节点就是右旋,待旋转的节点从右边上升到父节点就是左旋。
红黑树的修改操作和BST的操作类似,只是在修改后为避免出现倾斜进行修复操作。
插入
在插入新节点时,这个节点应该是红色,如果插入的节点是黑色,那么这个节点所在路径比其他路径多出一个黑色节点,这个调整起来会比较麻烦。
如果插入的节点是红色,此时所有路径上的黑色节点数量不变,仅可能会出现两个连续的红色节点的情况。这种情况下,通过变色和旋转进行调整即可,
删除
具体参考
数据结构可视化
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));
无法复制加载中的内容