集合与数据结构

List 接口

储存的对象有序,可重复

ArrayList

像数组一样存储对象,每次增删都需要构建新数组,适合多次查询的情况

构造方法:构造空数组,传入参数后构造一个默认为10大小的对象数组

add:如果没有超过数组大小,加入

超过,进入 grow 扩容方法

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

先将传入大小与原来数组大小的1.5倍(位运算,oldCapacity >> 1)进行比较,获取两者较大值。再将较大值与设定的最大数组长度(INTEGER.MAX_VALUE - 8)进行比较。如果没超过,以较大值构造数组,并复制原数组

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

如果超过,进行巨型容量处理
比较传入大小与最大数组长度,如果大于,构造 INTEGER.MAX_VALUE 长度数组,如果小于,构造最大数组长度数组

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) 
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

remove:删除数据,将后面的数前移,代码异常简单,将指定索引后面的元素拷贝到前一格

    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

LinkedList

像链表一样存储对象,适合多次增删的情况

内置内部类 node,是双向节点

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

Collection 集合遍历

1,使用 Iterator 迭代器
2,使用 foreach 遍历集合,不能修改数据,只能查询
3,使用 forEach 方法

  • 使用迭代器的 forEachRemaining 方法
  • 使用集合的 forEach 方法
  • 两者都有函数式接口,配合 Lambda 表达式使用

RandomAccess 接口

接口里没有任何属性和方法,只是一个标识

如果接入,表示此类支持随机访问,可以像数组一样查找数据,此时随机访问数据比较快

如果不接入,表示此类不支持随机访问,只能用链表方式查找数据,此时使用迭代器查找比较快

 if (list instanceof RandomAccess) {
      // 使用随机取值,即根据下标取值方式
} else {
     // Iterator遍历器取值
}

在实际应用中,当我们不明确获取到的是 Arraylist,还是 LinkedList 的时候,我们可以通过 RandomAccess 来判断其是否支持快速随机访问,若支持则采用 for 循环遍历,否则采用迭代器遍历

        if(list instanceof RandomAccess) {
            // for循环遍历
            for (int i = 0;i< list.size();i++) {
                System.out.println(list.get(i));
            }
        } else {
            // 迭代器遍历
            Iterator it = list.iterator();
            while(it.hasNext()){
                System.out.println(it.next());
            }
        }

Map 接口

map 集合储存键值对

LinkedHashMap

没有对 HashMap 的实现做任何修改,可以保持键值对的插入顺序。继承自 HashMap,将每个键值对当成一个节点,像链表一样储存

    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

HashMap

使用 hash 算法进行存储,输入的对象算出地址后,如果为空,直接存储,不为空,则视为哈希冲突

HashMap 多线程操作导致死循环问题

hashmap 本来就不能进行多线程的操作,多线程下不能使用 hashmap 的主要原因如下:

1,在添加数据时容易出现数据丢失的错误
2,当 hashmap 扩容时由于多个线程同时进行操作,底层代码的实现会让 hashmap 出现链表循环的问题,在执行扩容函数的时候没有上锁,多个线程同时无序的访问内存时就会出现这种经典的多线程问题

//不断的把老map中的元素取出放入新map中
void transfer(Entry[] newTable)
{
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

而具体表现就是在进行数据查找的时候会引发 CPU 被占满

基本属性

阈值:8,链表长度超过8进行扩容,如果数组长度不小于64此链表转化为红黑树(如果对红黑树中节点进行删除,节点减少至6会转会链表)
最大容纳:2的30次方
填充因子:0.75,储存结点超过数组大小乘填充因子时开始扩容
默认大小:16,初次构造数组大小

散列函数

《算法导论》中推荐的散列函数有三种:

1,除法散列法:通过取k除以m的余数,来将关键字k映射到m个槽的某一个中去
2,乘法散列法:首先,用关键字k乘上常数A(0<A<1),并抽取kA的小数部分;然后,用m乘以这个值,再取结果的底(即整数部分)
3,全域散列:在执行开始时,从一族仔细设计的函数中,随机地选择一个作为散列函数

在 java 中的散列函数是这个对象的 hashcode 异或 hashcode 二进制无符号右移16位

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

哈希表储存什么

储存 Node 节点,节点实现了 Entry 接口,因此哈希表可以返回键值对

哈希表开辟一块储存 Node 结点的连续内存空间,即 Node[]

可以储存 Node 与 TreeNode,分别对应链表与红黑树,TreeNode 是 Node 的子类

哈希冲突如何解决

1,链表法:把散列到同一槽中的所有元素都存放在一个链表中。每个槽中有一个指针,指向由所有散列到该槽的元素构成的链表的头,java 中就使用这种方法

2,开发寻址法:所有的元素都存放在散列表中,当要插入一个元素时,使用某种方法探查散列表的各项,直到找到一个空槽来放置待插入的关键字。而能使用的方法有很多。比如:

  • 线性寻址(h(k,i) = (h’(k) + i) mod m, i=0, 1, …, m-1,找散列表的下一个位置)
  • 二次寻址(h(k,i) = (h’(k) +c1i + c2i^2) mod m, i=0, 1, …, m-1,根据某个函数计算下一个位置)
  • 双重散列(h(k,i) = (h1(k) + i*h2(k)) mod m, i=0, 1, …, m-1,用第一个散列函数找到初始位置后,用第二个函数找其他位置)
  • 再哈希(h(k,i) = (h1(k)) ^ i mod m, i=0, 1, …, m-1,多次对结果通过散列函数进行计算)

那有没有完全避免 hash 冲突的方法呢,我们规定,如果某一种散列技术在进行查找时,其最坏情况内存访问次数为 O(1) 的话,则称其为完全散列。通常利用一种两级的散列方案,每一级上都采用全域散列。为了确保在第二级上不出现碰撞,需要让第二级散列表 Sj 的大小 mj 为散列到槽j中的关键字数 nj 的平方

源码分析

hash 函数:这个对象的 hashcode 异或 hashcode 二进制右移16位

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

put():调用 putVal 方法

putVal():

如果大小为空或0,调用 resize 方法扩容

调用hash计算哈希值,地址为空则插入

key值相同则替换

通过链表或红黑树查找,找到 key 值相同则替换

插入到末尾,此时链表长度大于8则尝试转为红黑树

通过数组长度与填充因子判断是否需要 resize 扩容

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)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

上面的代码是1.8之后的代码,它使用的是尾插法,在JDK8之前java使用的是头插法

resize:扩容机制

treeifyBin:链表转红黑树

为什么要从头插法改成尾插法?

头插法用不了红黑树了

为什么 HashMap 的默认长度是16

其实个人认为只要是2的幂就可以了,原因跟 hash 函数有关

因为经过 hash 函数计算之后,得到的结果会相当大,我们只取结果的后几位作为此对象应该存放的位置,比如如果 hashmap 的长度是16则是取 hash 函数计算结果的后4位,长度是32取后5位

如果是其他的数字作为 hashmap 的总大小,会导致存放数据时操作麻烦。而如果换一个合适的 hash 函数来计算数据应该存放的位置,就不需要满足是2的幂了

常用方法

put 方法会返回该 key 原来对应的值,如果原来没有该值则返回 null

compute:compute() 方法对 hashMap 中指定 key 的值进行重新计算。如果 key 对应的 value 不存在,则返回该 null,如果存在,则返回通过 remappingFunction 重新计算后的值

        HashMap<String, Integer> prices = new HashMap<>();
        // 无值返回 null,有值计算鞋子打了10%折扣后的值
        int newPrice = prices.compute("Shoes", (key, value) -> value - value * 10/100);

merge:merge() 方法会先判断指定的 key 是否存在,如果不存在,则添加键值对到 hashMap 中。如果 key 对应的 value 不存在,则返回该 value 值,如果存在,则返回通过 remappingFunction 重新计算后的值

	// 无值时返回100,并且对 Shirt 这个 key 塞入100
	// 有值时返回 oldValue(原来在 map 里的值) 加 newValue(100),并且塞入两值的和
	int returnedValue = prices.merge("Shirt", 100, (oldValue, newValue) -> oldValue + newValue);

computeIfAbsent:computeIfAbsent() 方法对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hashMap 中。如果 key 对应的 value 不存在,则使用获取 remappingFunction 重新计算后的值,并保存为该 key 的 value,否则返回 value

        // 计算 Shirt 的值。如果没值返回 280,并且塞入 280,有值直接返回
        int shirtPrice = prices.computeIfAbsent("Shirt", key -> 280);

putIfAbsent:putIfAbsent() 方法会先判断指定的键(key)是否存在,不存在则将键/值对插入到 HashMap 中。如果所指定的 key 已经在 HashMap 中存在,返回和这个 key 值对应的 value, 如果所指定的 key 不在 HashMap 中存在,则返回 null

        sites.putIfAbsent(4, "Weibo");

TreeMap

使用红黑树存储键值对
自平衡排序二叉树

源码分析

在调用put方法时,首先执行查找算法,需要判断有没有比较器

            do {
                parent = t;
                //如果没有比较器,执行内嵌的比较方法
                //cmp = k.compareTo(t.key);
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else {
                    V oldValue = t.value;
                    if (replaceOld || oldValue == null) {
                        t.value = value;
                    }
                    return oldValue;
                }
            } while (t != null);

找到要插入的位置后,执行addEntry方法,这个方法先将节点插入,然后执行红黑树的自平衡旋转

    private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (addToLeft)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
    }

Set接口

储存的对象无序且不能重复

HashSet

使用 hash 算法进行存储,输入的对象算出地址后,如果为空,直接存储,不为空则不能存储

内部直接调用 HashMap,键就是 set 中的值,值是一个 Object 对象

	private static final Object PRESENT = new Object();

    private transient HashMap<E,Object> map;
    
    public HashSet() {	 map = new HashMap<>();}

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

TreeSet

使用红黑树来进行存储,因此可以排序,排序分为:自然排序,定制排序

实现是调用NavigableMap(继承sortedMap的一个接口),真正的实现是TreeMap

Map 的集合遍历

1,集合自身 forEach 方法
2,Map 有自己的返回值为 set 集合的方法,可以对其用 Collection 集合遍历的方法进行遍历:keySet、valueSet、entrySet
3,for循环遍历
4,迭代器

在上面的说明中,树这个概念出现了多次,它是一个很有用的数据结构,可以衍生出优先队列、二叉搜索树等可以方便查找特定元素的数据结构

二叉搜索树

它或者是一棵空树,或者是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
  • 它的左、右子树也分别为二叉排序树

遍历

遍历二叉树,就是以某种方式逐个访问二叉树的每一个节点。根据访问树根的顺序,我们有三种方式遍历二叉树,无论哪种遍历方式,左节点一定在右节点之前:

1,前序遍历:其中间节点在左节点与右节点之前
2,中序遍历:其中间节点在左节点与右节点之中
3,后序遍历:其中间节点在左节点与右节点之后

所有的遍历都可以使用递归简单的实现

删除

由于二叉搜索树的插入与查询算法非常简单,这里只说明删除算法,删除分为以下三种情况,只有其中一种较为棘手:

1,当被删除节点z没有任何孩子时,直接删除即可
2,当被删除节点z只有一个孩子时,用这个唯一的孩子代替z
3,当被删除节点z有两个孩子时,寻找z的后继节点y,后继节点y是z的右子树中的最左边的节点,该节点没有左孩子。用该节点的右孩子代替该节点(如果没有右节点也没关系,使用NIL代替),然后将z中的数字直接改成y中的数字即可

就算不进行算法分析,也可以轻松的得出,二叉搜索树的插入查询与删除算法都是O(h),h为树高

红黑树

红黑树是一颗高度平衡的二叉搜索树,满足以下性质:

1,根节点与叶节点都是黑色的
2,红色节点的子节点都是黑色的
3,每个节点到叶节点的所以路径都包含相同数目的黑色节点

红黑树依靠左旋、右旋和变色在添加和删除时维持红黑树的性质

迭代器

说了这么多集合,来说说迭代器相关的知识吧

迭代器一般都有这三个方法,创建时获取该集合的迭代器进行循环即可,非常简单

E next()     会返回迭代器的下一个元素,并且更新迭代器的状态。在使用 next() 方法前必须使用 hasNext() 方法判断集合中是否还有下一个元素,否则可能会出现异常。
boolean hasNext()  用于检测集合中是否还有元素。存在则返回truedefault void remove()     删除上次调用next方法时返回的元素。注意,使用remove时,不能将迭代器置于第一位,否则会报错。

Collections

Collections中包含了一些对于集合的基本操作

static final <T> List<T> emptyList() || emptySet() || emptyMap():返回一个新的空的list、set、map集合,注意,这些集合都是不可以写入的

static <T> Set<T> singleton(T o) || singletonList(...) || singletonMap(...):返回一个新的只有一个元素的list、set、map集合,注意,这些集合都是不可以写入的

static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key):二分查找

static void shuffle(List<?> list):打乱一个集合的元素顺序

static <T> void sort(List<T> list, Comparator<? super T> c):选择一个集合按规定排序
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值