Java数据结构(2019.4.9)

前言

       本博客为个人复习时总结用,无商业目的,其大多数内容皆为博主整理所得,并非原创。侵删。


String和StringBuilder StringBuffer

这三个类之间的区别主要是在两个方面,即运行速度和线程安全这两方面。

首先说运行速度,或者说是执行速度,在这方面运行速度快慢为:StringBuilder > StringBuffer > String

String最慢的原因:

        String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的。以下面一段代码为例:

	String str="abc";
	System.out.println(str);
	str=str+"de";
	System.out.println(str);	

        如果运行这段代码会发现先输出“abc”,然后又输出“abcde”,好像是str这个对象被更改了,其实,这只是一种假象罢了,JVM对于这几行代码是这样处理的,首先创建一个String对象str,并把“abc”赋值给str,然后在第三行中,其实JVM又创建了一个新的对象也名为str,然后再把原来的str的值和“de”加起来再赋值给新的str,而原来的str就会被JVM的垃圾回收机制(GC)给回收掉了,所以,str实际上并没有被更改,也就是前面说的String对象一旦创建之后就不可更改了。所以,Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。

        而StringBuilder和StringBuffer的对象是变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。

        另外,有时候我们会这样对字符串进行赋值

String str="abc"+"de";
StringBuilder stringBuilder=new StringBuilder().append("abc").append("de");
System.out.println(str);
System.out.println(stringBuilder.toString());

        这样输出结果也是“abcde”和“abcde”,但是String的速度却比StringBuilder的反应速度要快很多,这是因为第1行中的操作和 String str=“abcde”; 是完全一样的,所以会很快,而如果写成下面这种形式

String str1="abc";
String str2="de";
String str=str1+str2;

        那么JVM就会像上面说的那样,不断的创建、回收对象来进行这个操作了。速度就会很慢。

再来说线程安全

        在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的.
        如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。

总结一下

String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

注:
        synchronized锁住的是代码还是对象。答案是:synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。
        当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。


值传递与引用传递:

  1. 传递值的数据类型:八种基本数据类型和String(这样理解可以,但是事实上String也是传递的地址,只是string对象和其他对象是不同的,string对象是不能被改变的,内容改变就会产生新对象。那么StringBuffer就可以了,但只是改变其内容。不能改变外部变量所指向的内存地址)。
  2. 传递地址值的数据类型:除String以外的所有复合数据类型,包括数组、类和接口。

int 与 integer区别:

  • 无论如何,Integer(integer i5 = 127;)与new Integer(Integer i8 = new Integer(127);)不会相等。不会经历拆箱过程,new出来的对象存放在堆,而非new的Integer常量则在常量池(在方法区),他们的内存地址不一样,所以为false。
  • 两个都是非new出来的Integer,如果数在-128到127之间,则是true,否则为false。因为java在编译Integer i2 = 128的时候,被翻译成:Integer i2 = Integer.valueOf(128);而valueOf()函数会对-128到127之间的数进行缓存。
  • 两个都是new出来的,都为false。还是内存地址不一样。
  • int和Integer(无论new否)比,都为true,因为会把Integer自动拆箱为int再去比。

集合容器

集合框架1
集合容器2
集合框架3
集合框架4
集合容器5

  • Iterator和ListIterator的区别:
    Iterator可用来遍历Set和List集合,hasNext()和next(),但是ListIterator只能用来遍历List。
    Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。(hasPrevious()和previous()方法)

Array和ArrayList,LinkedList:

  • Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
  • Array大小是固定的,ArrayList的大小是动态变化的。
  • ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。
    对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。
  • ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。
  • 相对于ArrayList,LinkedList的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。
  • LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。

List总结:

        所有的List中只能容纳单个不同类型的对象组成的表,而不是Key-Value键值对。例如:[ tom,1,c ]
        所有的List中可以有相同的元素,例如Vector中可以有 [ tom,koo,too,koo ]
所有的List中可以有null元素,例如[ tom,null,1 ]
        基于Array的List(Vector,ArrayList)适合查询,而LinkedList 适合添加,删除操作 。


ArrayList

  • ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。

  • ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。

  • ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。稍后,我们会比较List的“快速随机访问”和“通过Iterator迭代器访问”的效率。

  • ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。

  • ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。

  • 和Vector不同,ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。

  • ArrayList 实际上是通过一个数组去保存数据的。当我们构造ArrayList时;若使用默认构造函数,则ArrayList的默认容量大小是10。

  • 当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=“(原始容量x3)/2 + 1”。

  • ArrayList的克隆函数,即是将全部元素克隆到一个数组中。

  • ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。


Vector

  • Vector 是矢量队列,它是JDK1.0版本添加的类。继承于AbstractList,实现了List, RandomAccess, Cloneable这些接口。

  • Vector 继承了AbstractList,实现了List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。

  • Vector 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在Vector中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。

  • Vector 实现了Cloneable接口,即实现clone()函数。它能被克隆。
    和ArrayList不同,Vector中的操作是线程安全的。

  • Vector实际上是通过一个数组去保存数据的。当我们构造Vecotr时;若使用默认构造函数,则Vector的默认容量大小是10。
            当Vector容量不足以容纳全部元素时,Vector的容量会增加。若容量增加系数 >0,则将容量的值增加“容量增加系数”;否则,将容量大小增加一倍。
    Vector的克隆函数,即是将全部元素克隆到一个数组中。


Stack

Stack是栈。它的特性是:先进后出(FILO, First In Last Out)。
java工具包中的Stack是继承于Vector(矢量队列)的,由于Vector是通过数组实现的,这就意味着,Stack也是通过数组实现的,而非链表。当然,我们也可以将LinkedList当作栈来使用!


LinkedList

  • LinkedList 实际上是通过双向链表去实现的。
    它包含一个非常重要的内部类:Entry。Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。
  • 从LinkedList的实现方式中可以发现,它不存在LinkedList容量不足的问题。
  • LinkedList的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。
  • LinkedList实现java.io.Serializable。当写入到输出流时,先写入“容量”,再依次写入“每一个节点保护的值”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
  • 由于LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。

HashMap:

线程不安全的,可以用

Map map = Collections.synchronizedMap(new HashMap());

来获取线程安全Map。

HashMap主体为Entry;
HashMap
        当系统决定存储 HashMap 中的 key-value 对时,完全没有考虑 Entry 中的 value,仅仅只是根据 key 来计算并决定每个 Entry 的存储位置。这也说明了前面的结论:我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
        HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。
        HashMap中则通过h&(length-1)的方法来代替取模。

HashMap中put方法实现:
public V put(K key, V value) {  
    // 如果table引用指向成员变量EMPTY_TABLE,那么初始化HashMap(设置容量、临界值,新的Entry数组引用)
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 若“key为null”,则将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头
    // 所以table[0]的位置上,永远最多存储1个Entry对象,形成不了链表。key为null的Entry存在这里 
    if (key == null)  
        return putForNullKey(value);  
    // 若“key不为null”,则计算该key的哈希值
    int hash = hash(key);  
    // 搜索指定hash值在对应table中的索引
    int i = indexFor(hash, table.length);  
    // 循环遍历table数组上的Entry对象,判断该位置上key是否已存在
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
        Object k;  
        // 哈希值相同并且对象相同
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
            // 如果这个key对应的键值对已经存在,就用新的value代替老的value,然后退出!
            V oldValue = e.value;  
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;  
        }  
    }  
    // 修改次数+1
    modCount++;
    // table数组中没有key对应的键值对,就将key-value添加到table[i]处 
    addEntry(hash, key, value, i);  
    return null;  
}  

        可以看到,当我们给put()方法传递键和值时,HashMap会由key来调用hash()方法,返回键的hash值,计算Index后用于找到bucket(哈希桶)的位置来储存Entry对象。
        如果两个对象key的hash值相同,那么它们的bucket位置也相同,但equals()不相同,添加元素时会发生hash碰撞,也叫hash冲突,HashMap使用链表来解决碰撞问题。
        分析源码可知,put()时,HashMap会先遍历table数组,用hash值和equals()判断数组中是否存在完全相同的key对象,如果这个key对象在table数组中已经存在,就用新的value代替老的value。如果不存在,就创建一个新的Entry对象添加到table[ i ]处。
        如果该table[ i ]已经存在其他元素,那么新Entry对象将会储存在bucket链表的表头,通过next指向原有的Entry对象,形成链表结构(hash碰撞解决方案)。

查找:HashMap的查找时间复杂度只有在最理想的情况下才会为O(1)
public V get(Object key) {  
        if (key == null)  
            return getForNullKey();  
        int hash = hash(key.hashCode());  
        for (Entry<K,V> e = table[indexFor(hash, table.length)];  
             e != null;  
             e = e.next) {  
            Object k;  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
                return e.value;  
        }  
        return null;  
    }  

        分四步:

  1. 判断key,根据key算出索引。
  2. 根据索引获得索引位置所对应的键值对链表。
  3. 遍历键值对链表,根据key找到对应的Entry键值对。
  4. 拿到value。

hashmap

hash 算法:
static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

        如果key为空,那么hash值置为0。HashMap允许null作为键,虽然这样,因为null的hash值一定是0,而且null==null为真,所以HashMap里面最多只会有一个null键。而且这个null键一定是放在数组的第一个位置上。但是如果存在hash碰撞,该位置上形成链表了,那么null键对应的节点就不确定在链表中的哪个位置了(取决于插入顺序,并且每次扩容其在链表中的位置都可能会改变)。

        如果key是个不为空的对象,那么将key的hashCode值h和h无符号右移16位后的值做异或运算,得到最终的hash值。

计算下标:
/**
 * Returns index for hash code h.
 */
static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}
扩容:

        当HashMap的长度超出了加载因子与当前容量的乘积(默认16*0.75=12)时,通过调用resize方法重新创建一个原来HashMap大小的两倍的newTable数组,最大扩容到2^30+1,并将原先table的元素全部移到newTable里面,重新计算hash,然后再重新根据hash分配位置。这个过程叫作rehash,因为它调用hash方法找到新的bucket位置。

        java 1.8 以后变成了与操作,判断最高位

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        // 如果之前的HashMap已经扩充打最大了,那么就将临界值threshold设置为最大的int值
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        // 根据新传入的newCapacity创建新Entry数组
        Entry[] newTable = new Entry[newCapacity];
        // 用来将原先table的元素全部移到newTable里面,重新计算hash,然后再重新根据hash分配位置
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        // 再将newTable赋值给table
        table = newTable;
        // 重新计算临界值,扩容公式在这儿(newCapacity * loadFactor)
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

        数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这个操作是极其消耗性能的。所以如果我们已经预知HashMap中元素的个数,那么预设初始容量能够有效的提高HashMap的性能。

        重新调整HashMap大小,当多线程的情况下可能产生条件竞争。因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。

线程安全问题:

       HashMap是线程不安全的,在多线程情况下直接使用HashMap会出现一些莫名其妙不可预知的问题。在多线程下使用HashMap,有几种方案:

  1. 在外部包装HashMap,实现同步机制
  2. 使用Map m = Collections.synchronizedMap(new HashMap(…));实现同步(官方参考方案,但不建议使用,使用迭代器遍历的时候修改映射结构容易出错)
  3. 使用java.util.HashTable,效率最低(几乎被淘汰了)
  4. 使用java.util.concurrent.ConcurrentHashMap,相对安全,效率高(建议使用)

        注意一个小问题,HashMap所有集合类视图所返回迭代器都是快速失败的(fail-fast),在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器自身的 remove 或 add 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。。因此,面对并发的修改,迭代器很快就会完全失败。

        JDK1.8的HashMap源码实现和1.7是不一样的,有很大不同,其底层数据结构也不一样,引入了红黑树结构。
        当一个链表长度大于8的时候,HashMap会动态的将它替换成一个红黑树(JDK1.8引入红黑树大程度优化了HashMap的性能),这会将时间复杂度从O(n)降为O(logn)。


hashtable:

        HashTable和HashMap采用相同的存储机制,二者的实现基本一致,不同的是:

  1. HashMap是非线程安全的,HashTable是线程安全的,内部的方法基本都是synchronized。
  2. HashTable不允许有null值的存在。

ConcurrentHashMap

  • 首先将数据分成一段一段地存储
  • 然后给每一段数据配一把锁
  • 当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问
    concurrentHashMap
            ConcurrentHashMap是由Segment数组和HashEntry数组组成.
            Segment是一种可重入锁,在ConcurrentHashMap里扮演锁的角色;
            HashEntry则用于存储键值对数据.
            一个ConcurrentHashMap里包含一个Segment数组.
            Segment的结构和HashMap类似,是一种数组和链表结构.

        一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,
        必须首先获得与它对应的Segment锁

        Segment继承了ReentrantLock,因此它实际上是一把锁。在进行put、remove、replace、clear等需要改动内部内容的操作时,都要进行加锁操作,其代码一般是这样的:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
//实际代码……
        }
    } finally {
        unlock();
    }
    return oldValue;
}

        首先调用tryLock,如果加锁失败,则进入scanAndLockForPut(key, hash, value)
        该方法实际上是先自旋等待其他线程解锁,直至指定的次数MAX_SCAN_RETRIES
        若自旋过程中,其他线程释放了锁,导致本线程直接获得了锁,就避免了本线程进入等待锁的场景,提高了效率
        若自旋一定次数后,仍未获取锁,则调用lock方法进入等待锁的场景

扩容:

        ConcurrentLevel 一旦设定的话,就不会改变。ConcurrentHashMap当元素个数大于临界值的时候,就会发生扩容。但是ConcurrentHashMap与其他的HashMap不同的是,它不会对Segmengt 数量增大,只会增加Segmengt 后面的链表容量的大小。即对每个Segmengt 的元素进行的ReHash操作。

        主要就是因为扩展是按照2的幂次方进行扩展的,所以扩展前在同一个桶中的元素,现在要么还是在原来的序号的桶里,或者就是原来的序号再加上一个2的幂次方,就这两种选择。所以原桶里的元素只有一部分需要移动,其余的都不要移动。该函数为了提高效率,就是找到最后一个不在原桶序号的元素,那么连接到该元素后面的子链表中的元素的序号都是与找到的这个不在原序号的元素的序号是一样的那么就只需要把最后一个不在原序号的元素移到新桶里,那么后面跟的一串子元素自然也就连接上了,而且序号还是相同的。在找到的最后一个不在原桶序号的元素之前的元素就需要逐个的去遍历,加到和原桶序号相同的新桶上或者加到偏移2的幂次方的序号的新桶上。这个都是新创建的元素,因为只能在表头插入元素。

put:

        Put 时候 ,通过Hash函数将即将要put 的元素均匀的放到所需要的Segment 段中,调用Segment的put 方法进行数据。
        Segment的put 是加锁中完成的。如果当前元素数大于最大临界值的的话将会产生rehash. 先通过 getFirst 找到链表的表头部分,然后遍历链表,调用equals 比配是否存在相同的key ,如果找到的话,则将最新的Key 对应value值。如果没有找到,新增一个HashEntry 它加到整个Segment的头部。

get:
  1. 读取的时候 传递Key值,通过Hash函数计算出 对应Segment 的位置。
  2. 调用segmentFor(int hash) 方法,用于确定操作应该在哪一个segment中进行 ,通过 右无符号位运算 右移segmentShift位在与运算 segmentMask【偏移码】 获得需要操作的Segment

        确定了需要操作的Segment 再调用 get 方法获取对应的值。通过count 值先判断当前值是否为空。在调用getFirst()获取头节点,然后遍历列表通过equals对比的方式进行比对返回值。

       ConcurrentHashMap是分段并发分段进行读取数据的。
        Segment 里面有一个Count 字段,用来表示当前Segment中元素的个数 它的类型是volatile变量。所有的操作到最后都会 在最后一部更新count 这个变量,由于volatile变量 happer-before的特性。导致get 方法能够几乎准确的获取最新的结构更新。


TreeMap

  • TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。

  • TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。

  • TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。

  • TreeMap 实现了Cloneable接口,意味着它能被克隆。

  • TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。

  • TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。

  • TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
    另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。


Set总结:

  1. Set实现的基础是Map(HashMap)
  2. Set中的元素是不能重复的,如果使用add(Object obj)方法添加已经存在的对象,则会覆盖前面的对象。

HashSet:

static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

        我们知道HashSet的数据是存储在HashMap的实例对象map中的,并且对应于map中的key,Object类型的引用PRESENT则是对应于map中的value的一个虚拟值,没有实际意义。联想到HashMap的一些特性:无序存储、key值唯一等等,我们就可以很自然地理解Set集合元素不能重复以及HashSet无序存储的特性了。

        无get方法,只能通过迭代器顺序访问。

        HashSet是非同步的。如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:

Set s = Collections.synchronizedSet(new HashSet(...));

TreeSet

  • TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet, Cloneable, java.io.Serializable接口。
  • TreeSet 继承于AbstractSet,所以它是一个Set集合,具有Set的属性和方法。
  • TreeSet 实现了NavigableSet接口,意味着它支持一系列的导航方法。比如查找与指定目标最匹配项。
  • TreeSet 实现了Cloneable接口,意味着它能被克隆。
  • TreeSet 实现了java.io.Serializable接口,意味着它支持序列化。

        TreeSet是基于TreeMap实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。
        TreeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。
另外,TreeSet是非同步的。它的iterator 方法返回的迭代器是fail-fast的。


红黑树:

        红黑树的特性:

  1. 每个节点或者是黑色,或者是红色。
  2. 根节点是黑色。
  3. 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
  4. 如果一个节点是红色的,则它的子节点必须是黑色的。
  5. 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
(02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。
红黑树

定理:一棵含有n个节点的红黑树的高度至多为2log(n+1).

左旋
右旋


哈夫曼树:

        给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。

  1. 初始化:根据给定的n个权值,构造n棵二叉树的森林集合F={T1,T2,…,Tn},其中每棵二叉树只有一个权值为Wi的根节点,左右子树均为空。
  2. 找最小的树并构造新的树:在森林集合F中选取两颗根的权值最小的树作为左右子树构造一棵新的二叉树,新二叉树的根结点为新增加的结点,其权值为左右子树根的权值之和。
  3. 删除与插入:在森林集合中删除已选取的两棵根的权值最小的树,同时将新构造的二叉树加入到森林集合F中。
  4. 重复(2)和(3)步骤:直到森林集合只含有一棵树为止,这棵树即为哈夫曼树。
    哈夫曼树

二叉树:

  • 结点的度:结点拥有的子树的数目。

  • 叶子:度为零的结点。

  • 分支结点:度不为零的结点。

  • 树的度:树中结点的最大的度。

  • 层次:根结点的层次为1,其余结点的层次等于该结点的双亲结点的层次加1。

  • 树的高度:树中结点的最大层次。

  • 无序树:如果树中结点的各子树之间的次序是不重要的,可以交换位置。

  • 有序树:如果树中结点的各子树之间的次序是重要的, 不可以交换位置。

  • 森林:0个或多个不相交的树组成。对森林加上一个根,森林即成为树;删去根,树即成为森林。

  • 性质1:二叉树第i层上的结点数目最多为 2{i-1} (i≥1)。
  • 性质2:深度为k的二叉树至多有2{k}-1个结点(k≥1)。
  • 性质3:包含n个结点的二叉树的高度至少为log2 (n+1)。
  • 性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。
  • 在二叉查找树中:

(01) 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(02) 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(03) 任意节点的左、右子树也分别为二叉查找树。
(04) 没有键值相等的节点(no duplicate nodes)。

        如果二叉排序树是平衡的,则n个节点的二叉排序树的高度为Log2n+1,其查找效率为O(Log2n),近似于折半查找。如果二叉排序树完全不平衡,则其深度可达到n,查找效率为O(n),退化为顺序查找。一般的,二叉排序树的查找性能在O(Log2n)到O(n)之间。因此,为了获得较好的查找性能,就要构造一棵平衡的二叉排序树。


序列化与反序列化:

  • 序列化:把对象转换为字节序列的过程称为对象的序列化。
  • 反序列化:把字节序列恢复为对象的过程称为对象的反序列化。
序列化场景:
  • 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
  • 当你想用套接字在网络上传送对象的时候;
  • 当你想通过RMI传输对象的时候;
序列化实现:
  • 实现Serializable接口即可。
  • 静态static的属性不能被序列化,因为static不属于对象元素。也就是说序列化一个static的方法后,然后进行static属性修改,反序列化后不会覆盖修改过得属性。
  • 同理,transient 修饰的属性也不会被修改,如果直接序列化然后反序列化,则会丢失该属性值。
serialVersionUID

        序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值