链表/队列/栈/集合/Map/树/堆

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.Queue[I]
        +--java.util.Deque[I]   
        +--java.util.PriorityQueue[C]  
    +--java.util.Set [I]                   
       +--java.util.HashSet [C]    
          +--java.util.LinkedHashSet[C]  
       +--java.util.SortedSet [I]   
          +--java.util.TreeSet [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]:类

在这里插入图片描述

数组

  数组是相同数据类型的元素按一定顺序排序的集合,是一块连续的内存空间。Java中,Array就是数组,此外,ArrayList使用了数组Array作为其实现基础,它和一般的Array相比,最大的好处是,我们在添加元素时不必考虑越界,元素超出数组容量时,它会自动扩张保证容量。
  源代码:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

  ArrayList:get和add时间复杂度为O(1),remove时间复杂度为O(n)。
  Vector和ArrayList相比,主要差别就在于Vector是线程安全的,但是效率比较低下。如今java.util.concurrent包提供了许多线程安全的集合类(比如 LinkedBlockingQueue),所以不必再使用Vector了。

链表

  链表是一种非连续/非顺序的结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的。Java中,LinkedList 使用链表作为其基础实现。
  源代码:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable{}

  LinkedList:get时间复杂度为O(n),add和remove时间复杂度为O(1)。

LinkedList<String> 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");//删除

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

public class Stack<E> extends Vector<E> 

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

Deque<Integer> stack = new ArrayDeque<Integer>();
stack.push(12);//尾部入栈
stack.push(16);//尾部入栈
int tail = stack.pop();//尾部出栈,并删除该元素
tail = stack.peek();//尾部出栈,不删除该元素

当然了,也可以直接

Stack<Integer> s = new Stack<>();

队列

  队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,亦即所谓的先进先出(FIFO)。Java中,LinkedList实现了Deque接口,而Deque接口继承于Queue接口。 因此常用的队列操作是使用LinkedList来实现Queue

public interface Deque<E> extends Queue<E>
public interface Queue<E> extends Collection<E> 
//Deque<Integer> queue= new LinkedList<>();
Queue<Integer> queue= new LinkedList<>();
// 尾部入队,区别在于如果失败了
// add方法会抛出一个IllegalStateException异常,而offer方法返回false
integerDeque.qoffer(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();//返回第一个元素,不删除

  队列中还有一种特殊的结构就是PriorityQueue,队列的每一个元素都有优先级,且元素按照优先级排序。

public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable
public abstract class AbstractQueue<E> extends AbstractCollection<E> implements Queue<E> 

集合

  Set体系集合可以知道某物是否已近存在于集合中,不会存储重复的元素。加入Set的每个元素必须是唯一的,否则,Set是不会把它加进去的。要想加进Set,Object必须定义equals(),这样才能标明对象的唯一性。Set的接口和Collection的一摸一样。Set的接口不保证它会用哪种顺序来存储元素。

public interface Set<E> extends Collection<E>

继承子类

  • HashSet : 为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。快速高效
  • TreeSet : 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。较慢但可以自定义排序顺序
  • LinkedHashSet : 具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。快速插入顺序保存

HashSet

  底层是由HashMap实现的,通过对象的hashCode方法与equals方法来保证插入元素的唯一性,无序(存储顺序和取出顺序不一致).
增/删/查的时间复杂度均为O(1);

public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable

  其中AbstractSet是Set的抽象类

        // 1. initialize the hash set
        Set<Integer> hashSet = new HashSet<>();
        // 2. add a new key
        hashSet.add(3);
        hashSet.add(2);
        hashSet.add(1);
        // 3. remove the key
        hashSet.remove(2);
        // 4. check if the key is in the hash set
        if (!hashSet.contains(2)) {
            System.out.println("Key 2 is not in the hash set.");
        }
        // 5. get the size of the hash set
        System.out.println("The size of has set is: " + hashSet.size());
        // 6. iterate the hash set
        for (Integer i : hashSet) {
            System.out.print(i + " ");
        }
        System.out.println("are in the hash set.");
        // 7. clear the hash set
        hashSet.clear();
        // 8. check if the hash set is empty
        if (hashSet.isEmpty()) {
            System.out.println("hash set is empty now!");
        }

LinkedHashSet

  LinkedHashSet继承了HashSet,底层实现是哈希表和链表,保持了HashSet的速度,还能按照插入元素的顺序维持元素顺序使用。增/删/查的时间复杂度均为O(1);

public class LinkedHashSet<E> extends HashSet<E> implements Set<E>, Cloneable, java.io.Serializable {

TreeSet

  底层实现是红黑树(自平衡二叉树),不但能保证元素唯一,还能元素保证有序,存放到TreeSet中的元素应该实现Comparable接口,重写compareTo方法,否则会抛出ClassCastException,按照该方法指定的规则维持元素的顺序
增/删/查的时间复杂度均为O(logn)

public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable

Map

  Map 集合类用于存储元素对(称作“键”和“值”),其中每个键映射到一个值。
在这里插入图片描述
  通用方法

abstract void                 clear()
abstract boolean              containsKey(Object key)
abstract boolean              containsValue(Object value)
abstract Set<Entry<K, V>>     entrySet()
abstract boolean              equals(Object object)
abstract V                    get(Object key)
abstract int                  hashCode()
abstract boolean              isEmpty()
abstract Set<K>               keySet()
abstract V                    put(K key, V value)
abstract void                 putAll(Map<? extends K, ? extends V> map)
abstract V                    remove(Object key)
abstract int                  size()
abstract Collection<V>        values()

HashMap

  线程不安全的,增删改查的时间复杂度都是O(1)

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

LinkedHashMap

  继承于HashMap,线程不安全的,底层实现是HashMap和链表,元素按照插入顺序有序保存。增删改查的时间复杂度都是O(1)

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

TreeMap

  基于红黑树(一种自平衡二叉查找树)实现的,元素按照自定义的比较器有序保存。时间复杂度平均能达到O(log n)。

public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable

HashTable

  hashTable将key和value结合起来构成键值对通过put(key,value)方法保存起来,然后通过get(key)方法获取相对应的value值。它是线程安全的哈希映射表,内部采用Entry[]数组,每个Entry均可作为链表的头,用来解决冲突(碰撞)。

public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable

HashTable与HashMap的区别

  1. HashTable线程安全,HashMap线程不安全
  2. HashMap可以允许存在一个为null的key和任意个为null的value,但是HashTable中的key和value都不允许为null。当HashMap遇到为null的key时,它会调用putForNullKey方法来进行处理。对于value没有进行任何处理,只要是对象都可以。

二叉树

  二叉树的定义:二叉树的每个结点至多只有二棵子树(不存在度大于2的结点),二叉树的子树有左右之分,次序不能颠倒。二叉树的第i层至多有2i-1个结点;深度为k的二叉树至多有2k-1个结点;对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。
  二叉树的性质:

  1. 在非空二叉树中,第i层的结点总数不超过2i-1, i>=1;
  2. 深度为h的二叉树最多有2h-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。

满二叉树和完全二叉树:

  满二叉树: 除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。也可以这样理解,除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上。

满二叉树的性质:

1) 一颗树深度为h,最大层数为k,深度与最大层数相同,k=h;

2) 叶子数为2h;

3) 第k层的结点数是:2k-1;

4) 总结点数是:2k-1,且总节点数一定是奇数。

  完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。满二叉树是一种特殊的完全二叉树。
在这里插入图片描述

二叉查找树(BST)

  二叉查找树定义:又称为是二叉排序树(Binary Sort Tree)或二叉搜索树。二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
  1) 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2) 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
  3) 左、右子树也分别为二叉排序树;
  4) 没有键值相等的节点。
  二叉查找树的性质:对二叉查找树进行中序遍历,即可得到有序的数列。
  二叉查找树的时间复杂度:它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡。在最坏的情况下仍然有较好的时间复杂度,二叉查找树会退化成链表。
  二叉查找树的插入和删除的过程只需要牢记一点,插入和删除的操作都需要保持树的查找特性,即对于所有的节点:左子树<根<右子树
二叉查找树可以这样表示:

public class BST<Key extends Comparable<Key>, 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;
} 

平衡二叉树

  我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为log2n,其各操作的时间复杂度O(log2n)同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度。于是就有了我们下边介绍的平衡二叉树。

  平衡二叉树定义:平衡二叉树(Balanced Binary Tree)且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

AVL树

  AVL树定义:AVL树是最先发明的自平衡二叉查找树。在AVL中任何节点的两个儿子子树的高度最大差别为1,所以它也被称为高度平衡树,n个结点的AVL树最大深度约1.44log2n。查找、插入和删除在平均和最坏情况下都是O(logn)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。

  AVL树的自平衡操作——旋转:

  AVL树最关键的也是最难的一步操作就是旋转。旋转主要是为了实现AVL树在实施了插入和删除操作以后,树重新回到平衡的方法。
对于一个平衡的节点,由于任意节点最多有两个儿子,因此高度不平衡时,此节点的两颗子树的高度差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子树,所以属于左左情况。
在这里插入图片描述

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

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

双旋转

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

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

红黑树

  红黑树的定义:红黑树是一种自非严格平衡二叉查找树,在实践中是高效的: 红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的只读操作与普通二叉查找树上的只读操作相同。然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。恢复红黑树的性质需要少量(O(logn))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操作时间仍可以保持为O(logn) 次。

  红黑树的性质:

  红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制的一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  • 性质1. 节点是红色或黑色。
  • 性质2. 根是黑色。
  • 性质3. 所有叶子都是黑色(叶子是NIL节点)。
  • 性质4. 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  • 性质5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

下面是一个具体的红黑树的图例:
在这里插入图片描述

  这些约束确保了红黑树的关键特性: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

  要知道为什么这些性质确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

AVL树和红黑树的区别

  首先红黑树是不符合AVL树的平衡条件的,即每个节点的左子树和右子树的高度最多差1的二叉查找树。但是提出了为节点增加颜色,红黑是用非严格的平衡来换取增删节点时候旋转次数的降低,任何不平衡都会在三次旋转之内解决,而AVL是严格平衡树,因此在增加或者删除节点的时候,根据不同情况,旋转的次数比红黑树要多。所以红黑树的插入效率更高!!!

B树和B+树

B树(B-树)

B树是一种用于查找的平衡树,但是它不是二叉树。

  B树的定义:B树(B-tree)是一种树状数据结构,能够用来存储排序后的数据。这种数据结构能够让查找数据、循序存取、插入数据及删除的动作,都在对数时间内完成。B树,概括来说是一个一般化的二叉查找树,可以拥有多于2个子节点。与自平衡二叉查找树不同,B-树为系统最优化大块数据的读和写操作。B-tree算法减少定位记录时所经历的中间过程,从而加快存取速度。这种数据结构常被应用在数据库和文件系统的实作上。

  在B树中查找给定关键字的方法是,首先把根结点取来,在根结点所包含的关键字K1,…,Kn查找给定的关键字(可用顺序查找或二分查找法),若找到等于给定值的关键字,则查找成功;否则,一定可以确定要查找的关键字在Ki与Ki+1之间,Pi为指向子树根节点的指针,此时取指针Pi所指的结点继续查找,直至找到,或指针Pi为空时查找失败。

  B树作为一种多路搜索树(并不是二叉的):

1) 定义任意非叶子结点最多只有M个儿子;且M>2;

2) 根结点的儿子数为[2, M];

3) 除根结点以外的非叶子结点的儿子数为[M/2, M];

4) 每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个关键字)

5) 非叶子结点的关键字个数=指向儿子的指针个数-1;

6) 非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1];

7) 非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;

8) 所有叶子结点位于同一层;

B+树

  B+树是B树的变体,也是一种多路搜索树:

1) 其定义基本与B-树相同,除了:

2) 非叶子结点的子树指针与关键字个数相同;

3) 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间);

4) 为所有叶子结点增加一个链指针;

5) 所有关键字都在叶子结点出现;
  
  B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;

  B+的性质:

  • 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
  • 不可能在非叶子结点命中;
  • 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
  • 更适合文件索引系统。

首先需要说的是堆并不是java中的一种结构,堆是一颗完全二叉树,在这棵树中,所有父节点都满足大于等于其子节点的堆叫大根堆,所有父节点都满足小于等于其子节点的堆叫小根堆。堆虽然是一颗树,但是通常存放在一个数组中,父节点和孩子节点的父子关系通过数组下标来确定。

大顶堆

public class MaxHeap {
    /**
     * 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
     * 其中,N为数组下标索引值,如数组中第1个数对应的N为0。
     *
     * @param nums  数组
     * @param start 调整的起始位置,建堆的时候是中间开始,排序的时候是从头开始
     * @param end   截至范围(一般为数组中最后一个元素的索引)
     */
    public static void fixHeap(int[] nums, int start, int end) {
        int cur = start;
        int left = cur * 2 + 1;

        while (left <= end) {
            //这三个判断其实就是在维护三个数字那颗最小树
            //条件是为了避免越界风险
            if (left < end && nums[left] < nums[left + 1]) {
                left++;//比较大小取大的
            }
            //这里值比较了一次,是因为后面的值已经在树上了
            if (nums[cur] > nums[left]) {
                break;//调整结束
            } else {
                int temp = nums[cur];
                //交换位置
                nums[cur] = nums[left];
                nums[left] = temp;
            }
            //更新节点
            cur = left;
            left = left * 2 + 1;
        }
    }

    public static void creatHeap(int[] nums) {
        int n = nums.length;
        //建堆的时候从中间开始
        // 从(n/2-1) --> 0逐次遍历。遍历之后,得到的数组实际上是一个(最大)二叉堆。
        for (int i = (n - 1) / 2; i >= 0; i--) {
            //堆是一个完全二叉树,除了最后一层都是满的
            //从中间开始取,是因为是要去维护子堆
            fixHeap(nums, i, n - 1);
        }
    }

    public static void sortHeap(int[] nums) {
        //由于是大顶堆,堆顶(数组第一个元素)是最大的
        //因此升序排列的时候,每次将堆顶取出来放在最后,然后维护这个堆
        for (int i = nums.length - 1; i >= 0; i--) {
            int temp = nums[0];
            nums[0] = nums[i];
            nums[i] = temp;
            // 调整a[0...i-1],使得a[0...i-1]仍然是一个最大堆。
            // 即,保证a[i-1]是a[0...i-1]中的最大值。

            fixHeap(nums, 0, i - 1);
        }
    }
    public static void main(String[] args) {
        int nums[] = {20, 30, 90, 40,  60, 10, 100, 70, 110,50, 80};
        System.out.println("建堆");
        creatHeap(nums);
        System.out.println(Arrays.toString(nums));
        System.out.println("堆排");
        sortHeap(nums);
        System.out.println(Arrays.toString(nums));
    }

小顶堆

public class MinHeap {
    /**
     * 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。
     * 其中,N为数组下标索引值,如数组中第1个数对应的N为0。
     *
     * @param nums  数组
     * @param start 调整的起始位置,建堆的时候是中间开始,排序的时候是从头开始
     * @param end   截至范围(一般为数组中最后一个元素的索引)
     */
    public static void fixHeap(int[] nums, int start, int end) {
        int cur = start;
        int left = cur * 2 + 1;
        int temp = nums[cur];

        while (left <= end) {
            //这三个判断其实就是在维护三个数字那颗最小树
            //条件是为了避免越界风险
            if (left < end && nums[left] > nums[left + 1]) {   //改变一
                left++;//比较大小取小的
            }
            //这里值比较了一次,是因为后面的值已经在树上了
            if (temp <= nums[left]) {                          // 改变二
                break;//调整结束
            } else {
                //交换位置
                nums[cur] = nums[left];
                nums[left] = temp;
            }
            //更新节点
            cur = left;
            left = left * 2 + 1;
        }
    }

    public static void creatHeap(int[] nums) {
        int n = nums.length;
        //建堆的时候从中间开始
        // 从(n/2-1) --> 0逐次遍历。遍历之后,得到的数组实际上是一个(最大)二叉堆。
        for (int i = (n - 1) / 2; i >= 0; i--) {
            fixHeap(nums, i, n - 1);
        }
    }

    public static void sortHeap(int[] nums) {
        //由于是大顶堆,堆顶(数组第一个元素)是最大的
        //因此升序排列的时候,每次将堆顶取出来放在最后,然后维护这个堆
        for (int i = nums.length - 1; i >= 0; i--) {
            int temp = nums[0];
            nums[0] = nums[i];
            nums[i] = temp;
            // 调整a[0...i-1],使得a[0...i-1]仍然是一个最大堆。
            // 即,保证a[i-1]是a[0...i-1]中的最大值。

            fixHeap(nums, 0, i - 1);
        }
    }

    public static void main(String[] args) {

        int nums[] = {20, 30, 90, 40, 70, 110, 60, 10, 100, 50, 80};
        System.out.println("建堆");
        creatHeap(nums);
        System.out.println(Arrays.toString(nums));
        System.out.println("堆排");
        sortHeap(nums);
        System.out.println(Arrays.toString(nums));
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值