集合类

本文深入解析Java集合框架,包括Collection和Map接口的核心概念,详细介绍了List、Set、HashMap和TreeMap等常见集合类的实现原理及特性,如ArrayList的数组实现、LinkedList的双向链表结构、HashSet的去重机制、TreeSet的排序方式,以及HashMap的哈希表和链表结构,TreeMap的红黑树实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

类集实际上就是动态容器,说到容器,数组就是我接触到的第一个容器了,但是数组有一个很大的弊端,数组的长度固定,因此引出了类集的概念。

在java.util包里面有两个集合类核心接口:Collection和Map。

1.Collection

其中List,Set为常用的集合类。

1.1.List

其中List与Conllection最大的不同就是List接口中多了一个get(),用来根据索引取得内容。可以由上图可以看出,AbstractList实现了List接口,而之后由子类ArrayList,Vetor,LinkedList 。

1.1.1ArrayList

ArrayList是一个泛型容器,新建ArrayList需要实例化泛型参数,比如:

ArrayList<Integer> inList= new   ArrayList <Integer>();

ArrayList中主要方法有:

public  boolean  add(E  e):添加元素到末尾

public  void  add(int  index,E  element):在指定位置插入元素

public  boolean  isEmpty():判断是否为空

public  int  size():获取长度

public  E  get(int  index):获取指定位置的元素

public  int  indexOf(Object  o):查找元素,找到返回索引位置,没找到返回-1

public  boolean  contains(Object  o):是否包含指定元素

public  int  lastIndexOf(Object  o):从后往前查找

public  E  remove(int  index):删除指定位置元素,返回值为被删对象

public  boolean  remove(Object  o):删除指定对象,只删除第一个相同的对象,返回值表示是否删除了该对象,如果值为null,则删除值为null的元素

public  void  clear():删除所有元素

public  E  set(int  index,E  element):修改指定位置的元素内容为element。

ArrayList的基本原理:

在ArrayList内部有一个数组  Object[] elementData,有一个记录实际的元素个数。

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

在添加元素的时候会首先检查容量是不是够用

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

上面就是确保容量,以及增加容量的一套机制;

先是计算容器的容量,当第一次向容器中添加元素的时候,会向其分配DEFAULT_CAPACITY(大小为10),接下来确定容器的确定大小(调用ensureExplicitCapacity())其中modCount++表示内部修改的次数。如果当前数组的长度小于需要的容量大小,就扩容(调用grow()),扩容的时候,先将容量扩大之原数组大小的1.5倍,如果还是不能够满足需求,就直接将数组容量扩大至需求的容量。

ArrayList是基于数组的容器,查找元素就只需要通过索引来查找,但是删除某一个元素的时候就得移动元素。

1.1.2 Vector

Vector同样也是一个泛型容器,在使用时同样需要确定泛型类型。

Vector中的用法几乎与ArrayList一样,扩容方法也几乎与ArrayList一样,但是Vector中的很多方法都加入了synchronize同步语句来保证线程安全。但是就是因为保证了线程安全,导致Vector的效率同ArrayList相比要慢上很多。

1.1.3 LinkedList

由上图可以看出LinkedList不仅实现了List接口还实现了Deque接口,所以LinkedList底层与ArrayList不同之处就是它使用的是一个双向链表。

所谓双向链表,就是一个节点中包含两个链接,一个指向前驱节点,一个指向后续节点。

这就是双向循环链表的一个节点,一个指向前驱节点(prev),一个指向后继节点(next),一个是保存的实际的元素(item)。在LinkedList里面还有三个实例变量:

transient int size = 0;
transient Node<E> first;
transient Node<E> last;

其中size表示链表长度,first指向头节点,last指向尾节点,初始值为null。

首先还是来看添加方法:

 public boolean add(E e) {
        linkLast(e);
        return true;
    }
 void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

分析上面的代码得知,add()方法主要就是调用了linkLast()方法:

1.l和last节点都指向原链表的尾节点;

2.创建一个新节点newNode;

3.使last指向所创造的尾节点;

4.如果l指向为null(未插入newNode之前是一个空链表),那么头节点first也指向新创建的节点,否则让原链表的尾节点的后续节点指向newNode。

5.增加链表大小,增加修改次数。

List<String>list = new LinkedList<String>();

list.add("a");

list.add("b");

可以看出,LinkedList查找元素必须从头开始遍历,但是删除元素不需要移动节点;相对于ArrayList来说,查找元素显得复杂一些,但是删除元素就简单一些。

1.2 Set

Set接口和List接口中最大的不同就是,Set接口中不可以添加重复的元素,而在Set接口下最常用的就是HashSet与TreeSet两个容器类。

1.2.1 HashSet

HashSet内部是用HashMap实现的(HashMap往下翻)

 private transient HashMap<E,Object> map;

HashSet的构造方法:

    public HashSet() {
        map = new HashMap<>();
    }
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

可以看出就是调用HashMap的构造方法。

再来看看add方法:

    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

add方法就是调用put方法,向put中添加键值对(e,PRESENT)

解读就是,向HashSet中添加值,就是向HashMap添加键,值就为固定的PRESENT

与HashMap类似,HashSet要求重写hashCode和equals方法,因此HashSet中的元素没有顺序。

HashSet可以方便高效地实现去重,集合运算等功能。

1.2.2 TreeSet

上面的HashSet存储时就是元素之间没有特定的顺序,因此有了TreeSet。

同样TreeSet就是由TreeMap实现的。

    private transient NavigableMap<E,Object> m;

    private static final Object PRESENT = new Object();
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }

那么同样add方法就是调用TreeMap的put方法

TreeMap中没有重复的元素,添加、删除、判断元素是否存在,效率都比较高,为O(log2(N)),N为元素个数。

2.Map

Map接口中有键值对的概念,一个键映射一个值,Map中的键不能重复,即一个键只能存储一份,Map接口以及其实现类如下图所示。

2.1 HashMap

HashMap实现了Map集合,也是通过键值对来保存数据的。在创建HashMap的时候就需要先确定泛型类型;

例如:Map<Integer,Integer> map = new HashMap<Integer,Integer>();

HashMap中的一些主要方法:

V put(K key,V value)//保存键值对,如果原来的Key有值,覆盖,再返回原来的值
V get(Object key)//根据键获取值,没找到,返回null;
V remove(Object key)://根据键删除键值对,返回key原来的值,如果不存在,返回null
int size();//查看Map中的键值对的个数
boolean isEmpty();//是否为空
boolean containsKey(Object key);//查看是否包含某个键
boolean containsValue(Object value);//查看是否包含某个值

HashMap的基本实现原理:

在HashMap中有以下成员变量:

transient  Node<K,V>[]  table;

transient  int  size;

int  threshold;

final  float  loadFactor;

table:是一个Node类型的数组,称为哈希表或哈希桶。

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

可以由上面的代码可以看出,table李存放的是一个一个单向链表的头节点,链表中的每个节点都是一个键值对。

table初始状态是一个空表,在插入第一个元素的时候就进行默认分配大小为16.table在增长的时候是按2的幂次方增长。

size: 表示实际键值对个数

threshold:表示阈值当键值对个数size大于等于阈值的时候就考虑进行扩展。阈值=table.size*loadFactor

loadFactor:负载因子,默认的负载因子为0.75.

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // DEFAULT_LOAD_FACTOR默认值为0.75
    }


    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)//MAXIMUM_CAPACITY默认值为2^30表示最大容量.
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }


    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

当你自己定义table的初始容量,以及负载因子的时候,会条用tableSizeFor()来保证容量大小为2的幂次方。

下面来一起看看HashMap是如何保存键值对的:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)//如果table为空,也就是第一次调用put。
            n = (tab = resize()).length;//那么就对分配空间大小,并计算所分配的大小
        if ((p = tab[i = (n - 1) & hash]) == null)//如果计算得到的应该插入元素的位置上没有Node
            tab[i] = newNode(hash, key, value, null);//就创造一个Node,将Node放入tab中
        else {//如果该位置有Node了
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//如果该位置上的hash码与待插元素的hash码相同,两者键相同,两者的值相同
                e = p;//那么就将该位置的元素赋值给e
            else if (p instanceof TreeNode)//如果该位置上的元素已经树化
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//那就插入一个TreeNode节点将该树化节点赋值给e
            else {//如果待插元素既不与定位到的元素相同,定位到的元素也没有进行树化(那就是一个链表)
                for (int binCount = 0; ; ++binCount) {//通过循环遍历到链表的末尾,取得链表的长度,
                    if ((e = p.next) == null) {//如果p节点的下一个节点是null
                        p.next = newNode(hash, key, value, null);//那么就将待插元素插到定位到的元素的后面
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st(如果链表长度大于等于8)
                            treeifyBin(tab, hash);//就将其树化
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))//如果带插入元素的hash码,键,值与链表中的元素相同
                        break;//就跳出循环
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;//记录e的value
                if (!onlyIfAbsent || oldValue == null)//如果onlyIfabsent为false,或者旧值为空
                    e.value = value;//就用新值覆盖
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;//增加修改次数
        if (++size > threshold)//如果当前桶的长度大于阈值
            resize();//扩容或树化
        afterNodeInsertion(evict);
        return null;
    }

可以通过上述代码清楚的看出整个put的流程:

1.计算键的哈希值。

2.根据取得的哈希值在table中寻找应该插入的位置。

     2.1如果得到的位置上没有元素,那么就将带插入元素直接插入到该位置上。

     2.2如果该位置上有元素

          2.2.1如果该位置上的元素的hash码,key,value与待插元素相同,那么就直接覆盖。

          2.2.2如果该位置上的元素已经树化,那么就直接将带插入元素插到该树化节点的后面。

          2.2.3如果该位置没有树化,且该位置节点与带插入元素不同:

               2.2.3.1直接将待插元素插入链表最后面,再判断当前链表的长度是否大于8,如果大于8就将其树化。

               2.2.3.2判断链表中是否有与带插入元素相同的元素,如果有就直接跳出循环。

3.增加修改次数,判断当前桶的长度是否大于阈值,如果大于阈值就扩容,或将其树化。

根据上面的代码解释,以及流程梳理,我们发现resize()方法就是HashMap扩容的根本:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//将table的赋给oldTab
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//如果oldTab为空,oldCap就为0,否则oldCap为oldTab的长度
        int oldThr = threshold;//oldThr为table的阈值
        int newCap, newThr = 0;
        if (oldCap > 0) {//如果oldTab的长度大于0
            if (oldCap >= MAXIMUM_CAPACITY) {//如果oldTab的长度大于2^30
                threshold = Integer.MAX_VALUE;//就将阈值设为最大值(意味着table将没法扩容)
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)//当oldCap大于16并且oldCap按照2的幂次方进行扩容之后小于2^30(oldCap扩容之后将值赋给newCap)
                newThr = oldThr << 1; // oldThr也按2的幂次方进行扩容并将值赋给newThr
        }
        else if (oldThr > 0) // 如果阈值大于0(table原本不为空)
            newCap = oldThr;//新容量就为旧阈值
        else {               // 如果oldCap,oldThr为0的时候
            newCap = DEFAULT_INITIAL_CAPACITY;//新容量采用默认值16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新阈值才用默认值12
        }
        if (newThr == 0) {//如果新的阈值为0
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);//当newCap和ft都满足规定值的时候计算得到新的阈值
        }
        threshold = newThr;//将新阈值赋给threshold
        @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建一个大小为新容量的桶
        table = newTab;//将新桶赋给table
        if (oldTab != null) {//如果旧的hash桶不为空
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {//将旧桶中的元素赋值给e,当e不为空的时候
                    oldTab[j] = null;//将旧桶中的元素清空
                    if (e.next == null)//如果e的下一个元素为空
                        newTab[e.hash & (newCap - 1)] = e;//通过hash值找到确定的位置将e放入新桶
                    else if (e instanceof TreeNode)//如果e为一棵树
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//将红黑树进行拆分
                    else { // e为普通链表
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {//如果e的hash值与旧数组按位与为0
                                if (loTail == null)//如果loTail为空;
                                    loHead = e;//那么将e的值赋给loHead
                                else//如果loTail不为空
                                    loTail.next = e;//将e的值赋给loTail的下一个元素
                                loTail = e;//将e赋值给loTail
                            }
                            else {//如果e的hash值与旧数组按位与不为0
                                if (hiTail == null)//hiTail为空
                                    hiHead = e;//将e的值赋给hiTail
                                else//hiTail不为空
                                    hiTail.next = e;//将e的值赋给hiTail的下一个元素
                                hiTail = e;//将e的值赋给hitail
                            }
                        } while ((e = next) != null);//遍历整个链表
                        if (loTail != null) {//如果loTail不为空
                            loTail.next = null;//将loTail的下一个元素置空
                            newTab[j] = loHead;//新桶中的该位置插入loHead
                        }
                        if (hiTail != null) {//hiTail不为空
                            hiTail.next = null;//将hiTail下一个元素置空
                            newTab[j + oldCap] = hiHead;//新桶中的该位置再加上原数组的长度的位置插入hiHead
                        }
                    }
                }
            }
        }
        return newTab;
    }

HashMap 中的元素是没有顺序的,因为Hash值是随机的。

HashMap不是线程安全的,因此Java中还有一个HashTable,里面用了Synchronize关键字,因此安全性能会比HashMap高一些,但是相对于效率HashMap就会更强一些。

HashMap可以允许Key和Value为null,但是,HashTable不允许Key和Value为null。

HashMap每次扩容后的容量要求是2的幂次方,但是HashTable没有要求。

HashMap在当链表长度大于等于8的时候会将其树化,但是HashTable没有这样的机制。

但是基本上现在不用HashTable了,因为还有一个ConcurrentHashMap,它也是线程安全的,但是,它的效率并不低,因为它采用的是分段锁,而HashTable采用的是锁住了整个Map。

2.2TreeMap

TreeMap与HashMap最大的不同就是,TreeMap的存储有序,但是HashMap的存储是无序的。这是因为TreeMap中有一个比较器。

TreeMap底层是用红黑树实现的,下面是TreeMap的内部组成:

    private final Comparator<? super K> comparator;

    private transient Entry<K,V> root;

comparator就是一个比较器,在构造方法中传递,如果没传,就为null。root就是指向树的根节点,从根节点可以访问到每个节点,节点类型为Entry。

    static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;
        Entry(K key, V value, Entry<K,V> parent) {
            this.key = key;
            this.value = value;
            this.parent = parent;
        }

每个节点除了Key,Value之外,还有三个引用,left(左孩子),right(右孩子),parent(父节点),对于根节点,父节点为null,对于叶子结点,每一个子孩子都为null。

下面看看TreeMap是如何存储元素的

 public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {//当添加第一个节点时(root为null)
            compare(key, key); // 这里其实是为了检查Key的类型和null

            root = new Entry<>(key, value, null);//新增一个节点,让root指向它
            size = 1;
            modCount++;
            return null;
        }
        int cmp;//保存比较结果
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {//当设置了comparator时
            do {
                parent = t;//先从根节点开始寻找
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)//比较的结果小于0
                    t = t.left;//就去左孩子找
                else if (cmp > 0)//如果大于0
                    t = t.right;//就去右孩子找
                else
                    return t.setValue(value);//如果为0,表示已经存在这个值,就设置值,再返回
            } while (t != null);//一直到t为空时,此时parent就指向带插入节点的父节点
        }
        else {//如果未设置比较器
            if (key == null)//如果key为空的时候
                throw new NullPointerException();//抛出异常
                Comparable<? super K> k = (Comparable<? super K>) key;//假设key实现了Comparable接口
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);//新创建一个节点
        if (cmp < 0)//比较结果小于0
            parent.left = e;//那么带插入节点就是parent的左节点
        else
            parent.right = e;//否则就是parent的右节点
        fixAfterInsertion(e);//调整树的大致平衡
        size++;
        modCount++;
        return null;
    }

稍微总结一下,基本思路就是:循环找到父节点,并插入作为其左孩子或者右孩子,然后调整保持树的大致平衡。

接下来看看根据键删除键值对的代码:

    public V remove(Object key) {
        Entry<K,V> p = getEntry(key);//根据key找到节点
        if (p == null)//该节点为空
            return null;

        V oldValue = p.value;//记录即将被删的节点的值
        deleteEntry(p);//调用该方法删除节点
        return oldValue;//返回被删除的节点的值
    }
    final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)//如果comparator不为空
            return getEntryUsingComparator(key);//调用此方法
        if (key == null)//如果key为空
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;//假设key实现了Comparable接口
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }

可以看出remove()主要调用的就是deleteEntry():

    private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;
        if (p.left != null && p.right != null) {//如果被删节点的左右孩子都不为空
            Entry<K,V> s = successor(p);//找到后继节点,并用后继节点覆盖待删除节点
            p.key = s.key;
            p.value = s.value;
            p = s;
        }

        /*下面为待删节点只有一个子孩子的情况*/
     Entry<K,V> replacement = (p.left != null ? p.left : p.right);//记录待删节点的某一个子孩子
        if (replacement != null) {//如果replacement不为空
  
            replacement.parent = p.parent;//那么replacement的父节点就为待删节点的
            if (p.parent == null)//如果待删节点的父节点为空(待删节点为根节点)
                root = replacement;//那么replacement为根节点
            else if (p == p.parent.left)//如果待删节点是左孩子
                p.parent.left  = replacement;//那么待删节点的父节点的左孩子为replacement
            else
                p.parent.right = replacement;//否则待删节点的父节点的右孩子为replacement

            // Null out links so they are OK to use by fixAfterDeletion.
            p.left = p.right = p.parent = null;//然后将待删节点的孩子节点,父节点置空

            // Fix replacement
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { // return if we are the only node.
            root = null;
        } else { //  No children. Use self as phantom replacement and unlink.
            if (p.color == BLACK)
                fixAfterDeletion(p);

            if (p.parent != null) {
                if (p == p.parent.left)
                    p.parent.left = null;
                else if (p == p.parent.right)
                    p.parent.right = null;
                p.parent = null;
            }
        }
    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)//如果该节点为空
            return null;
        else if (t.right != null) {//如果该节点的右孩子为空,找出右节点中的最小的节点
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {//否则从当前节点的父节点开始往上找,如果它是父节点的右孩子,则继续找父节点的右孩子,如果不是或者父节点为空,第一个非右孩子节点的父节点就被返回
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }
    

TreeMap是按键排序,为了按键有序,TreeMap要求键实现Comparable接口或通过构造方法提供一个Comparator对象。根据键查找,保存,删除的效率比较高,为O(h),h为树的高度,h为log2(N),N为节点数。

好了,不多说了,敲代码了(如果有Bug请留言告知,感谢)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值