前言

仿佛一下子,2017年就快过去一半了,研一马上就要成为过去式了,我打算抓住研一的尾巴,好好梳理一下数据结构与算法,毕竟这些基础知识是很重要的嘛。所以准备在这里搞一个系列的文章,以期透彻。

本系列将采用Java语言来进行描述。亦即总结常见的的数据结构,以及在Java中相应的实现方法,务求理论与实践一步总结到位。

首先给出Java集合框架的基本接口/类层次结构:

java.util.Collection [I]

+--java.util.List [I]

+--java.util.ArrayList [C]

+--java.util.LinkedList [C]

+--java.util.Vector [C] //线程安全

+--java.util.Stack [C] //线程安全

+--java.util.Set [I]

+--java.util.HashSet [C]

+--java.util.SortedSet [I]

+--java.util.TreeSet [C]

+--Java.util.Queue[I]

+--java.util.Deque[I]

+--java.util.PriorityQueue[C]

java.util.Map [I]

+--java.util.SortedMap [I]

+--java.util.TreeMap [C]

+--java.util.Hashtable [C] //线程安全

+--java.util.HashMap [C]

+--java.util.LinkedHashMap [C]

+--java.util.WeakHashMap [C]

[I]:接口

[C]:类

本图来源于网络。

数组

数组是相同数据类型的元素按一定顺序排列的集合,是一块连续的内存空间。数组的优点是:get和set操作时间上都是O(1)的;缺点是:add和remove操作时间上都是O(N)的。

Java中,Array就是数组,此外,ArrayList使用了数组Array作为其实现基础,它和一般的Array相比,最大的好处是,我们在添加元素时不必考虑越界,元素超出数组容量时,它会自动扩张保证容量。

Vector和ArrayList相比,主要差别就在于多了一个线程安全性,但是效率比较低下。如今java.util.concurrent包提供了许多线程安全的集合类(比如 LinkedBlockingQueue),所以不必再使用Vector了。

int[] ints = new int[10];

ints[0] = 5;//set

int a = ints[2];//get

int len = ints.length;//数组长度

链表

链表是一种非连续、非顺序的结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,链表由一系列结点组成。链表的优点是:add和remove操作时间上都是O(1)的;缺点是:get和set操作时间上都是O(N)的,而且需要额外的空间存储指向其他数据地址的项。

查找操作对于未排序的数组和链表时间上都是O(N)。

Java中,LinkedList 使用链表作为其基础实现。

LinkedList linkedList = new LinkedList<>();

linkedList.add("addd");//add

linkedList.set(0,"s");//set,必须先保证 linkedList中已经有第0个元素

String s = linkedList.get(0);//get

linkedList.contains("s");//查找

linkedList.remove("s");//删除

//以上方法也适用于ArrayList

队列

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,亦即所谓的先进先出(FIFO)。

Java中,LinkedList实现了Deque,可以做为双向队列(自然也可以用作单向队列)。另外PriorityQueue实现了带优先级的队列,亦即队列的每一个元素都有优先级,且元素按照优先级排序。

Deque integerDeque = new LinkedList<>();

// 尾部入队,区别在于如果失败了

// add方法会抛出一个IllegalStateException异常,而offer方法返回false

integerDeque.offer(122);

integerDeque.add(122);

// 头部出队,区别在于如果失败了

// remove方法抛出一个NoSuchElementException异常,而poll方法返回false

int head = integerDeque.poll();//返回第一个元素,并在队列中删除

head = integerDeque.remove();//返回第一个元素,并在队列中删除

// 头部出队,区别在于如果失败了

// element方法抛出一个NoSuchElementException异常,而peek方法返回null。

head = integerDeque.peek();//返回第一个元素,不删除

head = integerDeque.element();//返回第一个元素,不删除

栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。它体现了后进先出(LIFO)

的特点。

Java中,Stack实现了这种特性,但是Stack也继承了Vector,所以具有线程安全线和效率低下两个特性,最新的JDK8中,推荐用Deque来实现栈,比如:

Deque stack = new ArrayDeque();

stack.push(12);//尾部入栈

stack.push(16);//尾部入栈

int tail = stack.pop();//尾部出栈,并删除该元素

tail = stack.peek();//尾部出栈,不删除该元素

集合

集合是指具有某种特定性质的具体的或抽象的对象汇总成的集体,这些对象称为该集合的元素,其主要特性是元素不可重复。

在Java中,HashSet 体现了这种数据结构,而HashSet是在MashMap的基础上构建的。LinkedHashSet继承了HashSet,使用HashCode确定在集合中的位置,使用链表的方式确定位置,所以有顺序。TreeSet实现了SortedSet 接口,是排好序的集合(在TreeMap 基础之上构建),因此查找操作比普通的Hashset要快(log(N));插入操作要慢(log(N)),因为要维护有序。

HashSet integerHashSet = new HashSet<>();

integerHashSet.add(12121);//添加

integerHashSet.contains(121);//是否包含

integerHashSet.size();//集合大小

integerHashSet.isEmpty();//是否为空

散列表

散列表也叫哈希表,是根据关键键值(Keyvalue)进行访问的数据结构,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数。

Java中HashMap实现了散列表,而Hashtable比它多了一个线程安全性,但是由于使用了全局锁导致其性能较低,所以现在一般用ConcurrentHashMap来实现线程安全的HashMap(类似的,以上的数据结构在最新的java.util.concurrent的包中几乎都有对应的高性能的线程安全的类)。TreeMap实现SortMap接口,能够把它保存的记录按照键排序。LinkedHashMap保留了元素插入的顺序。WeakHashMap是一种改进的HashMap,它对key实行“弱引用”,如果一个key不再被外部所引用,那么该key可以被GC回收,而不需要我们手动删除。

HashMap hashMap = new HashMap<>();

hashMap.put(1,"asdsa");//添加

hashMap.get(1);//获得

hashMap.size();//元素个数

树(tree)是包含n(n>0)个节点的有穷集合,其中:

每个元素称为节点(node);

有一个特定的节点被称为根节点或树根(root)。

除根节点之外的其余数据元素被分为m(m≥0)个互不相交的结合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。

树这种数据结构在计算机世界中有广泛的应用,比如操作系统中用到了红黑树,数据库用到了B+树,编译器中的语法树,内存管理用到了堆(本质上也是树),信息论中的哈夫曼编码等等等等,在Java中TreeSet和TreeMap用到了树来排序(二分查找提高检索速度),不过一般都需要程序员自己去定义一个树的类,并实现相关性质,而没有现成的API。下面就用Java来实现各种常见的树。

二叉树

二叉树是一种基础而且重要的数据结构,其每个结点至多只有二棵子树,二叉树有左右子树之分,第i层至多有2^(i-1)个结点(i从1开始);深度为k的二叉树至多有2^(k)-1)个结点,对任何一棵二叉树,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。

二叉树的性质:

1) 在非空二叉树中,第i层的结点总数不超过2^(i-1), i>=1;

2) 深度为h的二叉树最多有2^h-1个结点(h>=1),最少有h个结点;

3) 对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;

4) 具有n个结点的完全二叉树的深度为log2(n+1);

5)有N个结点的完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系:

若I为结点编号则 如果I>1,则其父结点的编号为I/2;

如果2I<=N,则其左儿子(即左子树的根结点)的编号为2I;若2I>N,则无左儿子;

如果2I+1<=N,则其右儿子的结点编号为2I+1;若2I+1>N,则无右儿子。

6)给定N个节点,能构成h(N)种不同的二叉树,其中h(N)为卡特兰数的第N项,h(n)=C(2*n, n)/(n+1)。

7)设有i个枝点,I为所有枝点的道路长度总和,J为叶的道路长度总和J=I+2i。

满二叉树、完全二叉树

满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点;

完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树;

满二叉树是完全二叉树的一个特例。

二叉查找树

二叉查找树,又称为是二叉排序树(Binary Sort Tree)或二叉搜索树。二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:

1) 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

2) 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;

3) 左、右子树也分别为二叉排序树;

4) 没有键值相等的节点。

二叉查找树的性质:对二叉查找树进行中序遍历,即可得到有序的数列。

二叉查找树的时间复杂度:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡二叉树设计的初衷。

二叉查找树可以这样表示:

public class BST, Value> {

private Node root; // 根节点

private class Node {

private Key key; // 排序的间

private Value val; // 相应的值

private Node left, right; // 左子树,右子树

private int size; // 以该节点为根的树包含节点数量

public Node(Key key, Value val, int size) {

this.key = key;

this.val = val;

this.size = size;

}

}

public BST() {}

public int size() {//获得该二叉树节点数量

return size(root);

}

private int size(Node x) {获得以该节点为根的树包含节点数量

if (x == null) return 0;

else return x.size;

}

}

查找:

public Value get(Key key) {

return get(root, key);

}

private Value get(Node x, Key key) {//在以x节点为根的树中查找key

if (x == null) return null;

int cmp = key.compareTo(x.key);

if (cmp < 0) return get(x.left, key);//递归左子树查找

else if (cmp > 0) return get(x.right, key);//递归右子树查找

else return x.val;//找到了

}

插入:

public void put(Key key, Value val) {

root = put(root, key, val);

}

private Node put(Node x, Key key, Value val) {在以x节点为根的树中查找key,val

if (x == null) return new Node(key, val, 1);

int cmp = key.compareTo(x.key);

if (cmp < 0) x.left = put(x.left, key, val);//递归左子树插入

else if (cmp > 0) x.right = put(x.right, key, val);//递归右子树插入

else x.val = val;

x.size = 1 + size(x.left) + size(x.right);

return x;

}

删除:

public Key min() {

return min(root).key;

}

private Node min(Node x) {

if (x.left == null) return x;

else return min(x.left);

}

public void deleteMin() {

root = deleteMin(root);

}

private Node deleteMin(Node x) {//删除以x为根节点的子树最小值

if (x.left == null) return x.right;

x.left = deleteMin(x.left);

x.size = size(x.left) + size(x.right) + 1;

return x;

}

public void delete(Key key) {

root = delete(root, key);

}

private Node delete(Node x, Key key) {

if (x == null) return null;

int cmp = key.compareTo(x.key);

if (cmp < 0) x.left = delete(x.left, key);//递归删除左子树

else if (cmp > 0) x.right = delete(x.right, key);//递归删除右子树

else { //该节点就是所要删除的节点

if (x.right == null) return x.left;//没有右子树,把左子树挂在原节点父节点上

if (x.left == null) return x.right;//没有左子树,,把右子树挂在原节点父节点上

Node t = x;//用右子树中最小的节点来替代被删除的节点,仍然保证树的有序性

x = min(t.right);

x.right = deleteMin(t.right);

x.left = t.left;

}

x.size = size(x.left) + size(x.right) + 1;

return x;

}

平衡二叉树

平衡二叉树又被称为AVL树,具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。它的出现就是解决二叉查找树不平衡导致查找效率退化为线性的问题,因为在删除和插入之时会维护树的平衡,使得查找时间保持在O(logn),比二叉查找树更稳定。

ALLTree 的 Node 由 BST 的 Node 加上 private int height; 节点高度属性即可,这是为了便于判断树是否平衡。

维护树的平衡关键就在于旋转。对于一个平衡的节点,由于任意节点最多有两个儿子,因此高度不平衡时,此节点的两颗子树的高度差2.容易看出,这种不平衡出现在下面四种情况:

1、6节点的左子树3节点高度比右子树7节点大2,左子树3节点的左子树1节点高度大于右子树4节点,这种情况成为左左。

2、6节点的左子树2节点高度比右子树7节点大2,左子树2节点的左子树1节点高度小于右子树4节点,这种情况成为左右。

3、2节点的左子树1节点高度比右子树5节点小2,右子树5节点的左子树3节点高度大于右子树6节点,这种情况成为右左。

4、2节点的左子树1节点高度比右子树4节点小2,右子树4节点的左子树3节点高度小于右子树6节点,这种情况成为右右。

从图2中可以可以看出,1和4两种情况是对称的,这两种情况的旋转算法是一致的,只需要经过一次旋转就可以达到目标,我们称之为单旋转。2和3两种情况也是对称的,这两种情况的旋转算法也是一致的,需要进行两次旋转,我们称之为双旋转。

单旋转是针对于左左和右右这两种情况,这两种情况是对称的,只要解决了左左这种情况,右右就很好办了。图3是左左情况的解决方案,节点k2不满足平衡特性,因为它的左子树k1比右子树Z深2层,而且k1子树中,更深的一层的是k1的左子树X子树,所以属于左左情况。

为使树恢复平衡,我们把k1变成这棵树的根节点,因为k2大于k1,把k2置于k1的右子树上,而原本在k1右子树的Y大于k1,小于k2,就把Y置于k2的左子树上,这样既满足了二叉查找树的性质,又满足了平衡二叉树的性质。

这样的操作只需要一部分指针改变,结果我们得到另外一颗二叉查找树,它是一棵AVL树,因为X向上一移动了一层,Y还停留在原来的层面上,Z向下移动了一层。整棵树的新高度和之前没有在左子树上插入的高度相同,插入操作使得X高度长高了。因此,由于这颗子树高度没有变化,所以通往根节点的路径就不需要继续旋转了。

代码:

private int height(Node t){

return t == null ? -1 : t.height;

}

//左左情况单旋转

private Node rotateWithLeftChild(Node k2){

Node k1 = k2.left;

k2.left = k1.right;

k1.right = k2;

k1.size = k2.size;

k2.size = size(k2.right)+size(k2.left)+1;

k2.height = Math.max(height(k2.left), height(k2.right)) + 1;

k1.height = Math.max(height(k1.left), k2.height) + 1;

return k1; //返回新的根

}

//右右情况单旋转

private Node rotateWithRightChild(Node k2){

Node k1 = k2.right;

k2.right = k1.left;

k1.left = k2;

k1.size = k2.size;

k2.size = size(k2.right)+size(k2.left)+1;

k2.height = Math.max(height(k2.left), height(k2.right)) + 1;

k1.height = Math.max(height(k1.right), k2.height) + 1;

return k1; //返回新的根

}

双旋转是针对于左右和右左这两种情况,单旋转不能使它达到一个平衡状态,要经过两次旋转。同样的,这样两种情况也是对称的,只要解决了左右这种情况,右左就很好办了。图4是左右情况的解决方案,节点k3不满足平衡特性,因为它的左子树k1比右子树Z深2层,而且k1子树中,更深的一层的是k1的右子树k2子树,所以属于左右情况。

为使树恢复平衡,我们需要进行两步,第一步,把k1作为根,进行一次右右旋转,旋转之后就变成了左左情况,所以第二步再进行一次左左旋转,最后得到了一棵以k2为根的平衡二叉树树。

代码:

//左右情况

private Node doubleWithLeftChild(Node k3){

try{

k3.left = rotateWithRightChild(k3.left);

}catch(NullPointerException e){

System.out.println("k.left.right为:"+k3.left.right);

throw e;

}

return rotateWithLeftChild(k3);

}

//右左情况

private Node doubleWithRightChild(Node k3){

try{

k3.right = rotateWithLeftChild(k3.right);

}catch(NullPointerException e){

System.out.println("k.right.left为:"+k3.right.left);

throw e;

}

return rotateWithRightChild(k3);

}

AVL查找操作与BST相同,AVL的删除与插入操作在BST基础之上需要检查是否平衡,如果不平衡就要使用旋转操作来维持平衡:

private Node balance(Node x) {

if (balanceFactor(x) < -1) {//右边高

if (balanceFactor(x.right) > 0) {//右左

x.right = rotateWithLeftChild(x.right);

}

x = rotateWithRightChild(x);

}

else if (balanceFactor(x) > 1) {//左边高

if (balanceFactor(x.left) < 0) {//左右

x.left = rotateWithRightChild(x.left);

}

x = rotateWithLeftChild(x);

}

return x;

}

private int balanceFactor(Node x) {

return height(x.left) - height(x.right);

}

堆是一颗完全二叉树,在这棵树中,所有父节点都满足大于等于其子节点的堆叫大根堆,所有父节点都满足小于等于其子节点的堆叫小根堆。堆虽然是一颗树,但是通常存放在一个数组中,父节点和孩子节点的父子关系通过数组下标来确定。如下图的小根堆及存储它的数组:

值: 7,8,9,12,13,11

数组索引: 0,1,2,3, 4, 5

通过一个节点在数组中的索引怎么计算出它的父节点及左右孩子节点的索引:

public int left(int i){

return (i + 1) * 2 - 1;

}

public int right(int i){

return (i + 1) * 2;

}

public int parent(int i){

// i为根结点

if (i == 0) {

return -1;

}

return (i - 1) / 2;

}

维护大根堆的性质:

public void heapify(T[] a, int i, int heapLength) {

int l = left(i);

int r = right(i);

int largest = -1;

//寻找根节点及其左右子节点,三个元素中的最大值

if (l < heapLength && a[i].compareTo(a[l]) < 0) {

largest = l;

} else {

largest = i;

}

if (r < heapLength && a[largest].compareTo(a[r]) < 0) {

largest = r;

}

// 如果i处元素不是最大的,就把i处的元素与最大处元素交换,使得i处元素变为最大的

if (i != largest) {

T temp = a[i];

a[i] = a[largest];

a[largest] = temp;

// 交换元素后,以a[i]为根的树可能不在满足大根堆性质,于是递归调用该方法

heapify(a, largest, heapLength);

}

}

构造堆:

public void buildHeap(T[] a, int heapLength){

//从后往前看lengthParent处的元素是第一个有子节点的元素,所以从它开始,进行堆得维护

int lengthParent = parent(heapLength - 1);

for(int i = lengthParent; i >= 0; i--){

heapify(a, i, heapLength);

}

}

堆的用途:堆排序,优先级队列。此外由于调整代价较小,也适合实时类型的排序与变更。

后记

写着写着就发现要想总结到位是一项非常庞大的工程,路漫漫其修远兮。吾将上下而求索啊。

欢迎访问我的主页 mageek

参考与感谢:

数据结构和算法 java实现_数据结构与算法——常用数据结构及其Java实现相关推荐

  1. aes算法c语言实现_C语言实现常用数据结构:Dijkstra最短路径算法(第18篇)

    「今天是学习C语言第 161 天」 纸上学来终觉浅,绝知此事要躬行.-- 陆游「冬夜读书示子聿」#题外话算法学习重点是学习如何编程使用它. # Dijkstra算法 Dijkstra算法,中文译名迪杰 ...

  2. 沙普利算法的java实现_盖尔-沙普利算法告诉你,你的对象在哪里?

    假设有三男(分别是 A ,B ,C )和三女(分别是 x,y ,z ),他(她)们对异性的心仪程度如对话框所示. 比如对于男 A 来说,心仪对象排名为 x 排第一,y 排第二,z 排第三. 今天是特殊 ...

  3. 人工鱼群算法python代码_人工鱼群算法python_鱼群算法 - Brillou的个人空间 - OSCHINA - 中文开源技术交流社区......

    本算法是参照李晓磊博士的论文实现的,详细的算法原理可阅读<一种新型的智能优化方法_人工鱼群算法_李晓磊> 算法基于鱼群的生存行为:在一片水域中,鱼存在的数目最多的地方就是本水域中富含营养物 ...

  4. a*算法matlab代码_导向滤波算法及其matlab代码实现

    导向滤波同样是一种平滑滤波算法,其与最小二乘滤波和双边滤波相比,同样是一种具有边缘保持的功能的图形滤波算法,可以用于处理图形噪点较多的图像,而且此种滤波算法与最小二乘滤波和双边滤波相比,有其独特的特点 ...

  5. java 分析java死锁_有益的CountDownLatch和棘手的Java死锁

    java 分析java死锁 您是否曾经使用过java.util.concurrent.CountDownLatch ? 这是在两个或多个线程之间实现同步的非常方便的类,在该类中,一个或多个线程可以等待 ...

  6. aix系统java堆_浅谈AIX环境下的Java性能调优

    1.什么是Java Java 是一种面向对象的编程语言.它以 C++ 为模型,被设计成小的.简单的.在源和二进制级别跨平台的可移植的语言,Java 程序(applets 和应用程序)可以运行于任何已经 ...

  7. 安装java环境_(一)环境安装之Java

    安装java 点击 JDK8下载,根据自己的平台,选择相应的版本进行下载. 小知识: Java环境分JDK和JRE ,JDK就是Java Development Kit.简单的说JDK是面向开发人员使 ...

  8. prim算法适用条件_内部排序算法的比较及应用

    "内部排序包括        插入排序(直接插入排序.折半插入排序.希尔排序),        交换排序(冒泡排序.快速排序),        选择排序(简单选择排序.堆排序),       ...

  9. cmd编译java命令_使用CMD命令编译和运行Java程序

    对于初学者来说,使用CMD命令(Unix以及类Unix系统采用Termial)来编译和运行Java的好处是让初学者直观地体会到编译(Compile)这一步骤,加深记忆.所谓编译就是将文本文件xxx.j ...

  10. dv算法java实现_各种排序算法的分析及java实现(二)

    更多精彩,请点击上方蓝字关注我们! 上次跟大家分享了下各种排序算法的分析及java实现(一)的相关知识,今天跟大家分享各种排序算法的分析及java实现(二)的知识.昨天我们讲到了选择排序,今天我们继续 ...

最新文章

  1. PYTHON-模块timedatetime+ 目录规范
  2. java httpinvoker漏洞_Java反序列化漏洞学习
  3. 网校mysql设计规范_网校数据库设计
  4. 23种基本设计模式简介
  5. 运维前线:一线运维专家的运维方法、技巧与实践导读
  6. [html] 举例说明原生的html组件有哪些?
  7. Python库大全,建议收藏留用!
  8. VideoLan 0.8.6b test 1
  9. JESD204B调试4
  10. 最大公约数和最小公倍数的关系
  11. 什么是精细化营销和精准化营销 - whale 帷幄
  12. LTspice中 Voltage Controlled Switches的使用方法
  13. 网络工程师的python之路pdf_网络工程师的Python之路:网络运维自动化实战
  14. 用指定的用户名和密码无法登录到该FTP服务器
  15. RHCA 8环境说明
  16. springBoot集成swagger访问报404
  17. 十大算法展辉煌历史,十大问题引锦绣前程
  18. JAVA实现闯关小游戏(一)
  19. 【无标题】相册排版软件
  20. 普通人如何架设一个简单的服务器攻略

热门文章

  1. lxml解析html报错
  2. win7打开计算机没盘符,计算机U盘盘符不显示怎么办?Win7系统U盘不显解决办法...
  3. Linux中nohup命令使用介绍
  4. 学生实习质量跟踪系统
  5. 酒桶-最大上升序列问题(动态规划)
  6. Office 2010卸载工具(来自微软官方)
  7. wqs二分中二分条件细节的思考
  8. Ubuntu系统管理---配置VIM显示行号
  9. 历代OPPO Reno主要参数对比,更新于2021年1月
  10. Source Insight中显示函数调用关系的方法