第九章 集合


文章目录

  • 第九章 集合
  • `Java`集合框架
  • 集合接口与实现分离
  • `Collection`接口
  • 迭代器
  • 泛型实用方法
  • 集合框架中的接口
  • 具体集合
  • 链表
  • 数组列表
  • 散列集
  • 树集
  • 优先队列
  • 映射
  • 映射的基本操作
  • 更新映射条目
  • 映射视图
  • 弱散列试图
  • 链接散列集与映射
  • 枚举集与映射
  • 标识散列映射
  • 视图与包装器
  • 小集合
  • 子范围
  • 不可修改视图
  • 同步视图
  • 算法
  • 为什么使用泛型算法
  • 排序与混排
  • 二分查找
  • 简单算法
  • 批操作
  • 集合和数组的转换
  • 遗留的集合
  • 位集


Java集合框架

集合接口与实现分离

  • 队列,先进先出
  • 队列接口最简形式:
public interface Queue<E>{
    void add(E element);
    E remove();
    int size();
}
  • 队列实现: 1. 使用循环数组 2. 使用链表
public class CirclarArrayQueue<E> implements  Queue<E>{
    private int head;
    private int tail;
    CirclarArrayQueue(int capacity);
    public void add(E element);
    public E remove();
    public int size();
    private E[] element;
}

Collection接口

  • 集合类的基本接口是Collection接口
public interface Collection<E>{
    boolean add(E element);
    Iterator<E> iterator();
    ...
}

迭代器

  • Iterator接口包含
public interface Iterator<E>
{
    E next();
    boolean hasNext();
    void remove();
    default void forEachRemaining(Consumer<? super E> action);
}
  • 访问集合中的元素
Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while(iter.hasNext()){
    String element = iter.next();
    ...
}

或者直接
    for(String elem: c){
        ...
    }
  • for-each循环可以处理任何实现了Iterable接口的对象
public interface Iterable<E>{
    Iterator<E> iterator();
}

Collection接口扩展了Iterable接口,所以标准库中任何集合都有可以使用for-each循环

  • 也可以forEachRemaining(element->do sth. with element)对于迭代器每个元素调用这个lambda表达式直到没有元素为止
  • java迭代器位于两个元素之间,调用next,迭代器越过下一个元素, 并且返回刚刚越过元素的引用
  • Iterator接口的remove方法将删除上次调用next方法返回的元素
Iterator<Stirng> it = c.iterator();
it.next();
it.remove();
//删除该字符串的第一个元素
  • remove方法调用之前没有调用next方法会抛出IllegalStateException异常

泛型实用方法

  • P372

集合框架中的接口

  • 集合中有两个基本接口Collection, Map
  • 在集合中插入元素boolean add(E element); 映射包含键值对,插入用V put(K key, V value);
  • 从映射中读取值V get(K key);
  • List是一个有序集合,可以用迭代器访问,可以随机访问(访问整数索引)
  • Set接口等同于Colletion接口,其add方法不允许增加重复元素, hasCode保证包含相同元素得到相同的散列码,equals相等

具体集合

链表

  • java中, 所有链表实际上都是双向链接的
  • 先添加3个元素, 再将第二个元素删除
var a = new LinkedList<String>();
a.add("Amy");
a.add("Carl");
a.add("Erica");
Iterator<Stirng> iter = staff.iterator();
String first = iter.next();
String second = iter.next();
iter.remove();
  • 链表和泛型集合之间的区别: 链表是一个有序集合, LinkList.add方法将对象添加到链表尾部
  • 反向遍历链表: LinkIterator接口: E previous(); boolean hasPrevious();
  • 为了避免并发修改异常:可以根据需要为集合关联过多个迭代器,但是迭代器只能读取集合
  • LinkedList类提供了get方法访问某个特定元素,但是效率不高
package chapter9_set.linkedList;

import java.util.*;

public class LinkedListTest {
    public static void main(String[] args) {
        var a = new LinkedList<String>();
        a.add("Amy");
        a.add("Carl");
        a.add("Erica");

        var b = new LinkedList<String>();
        b.add("Bob");
        b.add("Doug");
        b.add("Frances");
        b.add("Gloria");

        //merge the words from b into a
        ListIterator<String> aIter = a.listIterator();

        Iterator<String> bIter = b.iterator();
        while (bIter.hasNext()) {
            if(aIter.hasNext()) aIter.next();
            aIter.add(bIter.next());
        }
        /*
        for (String s : b) {
            if (aIter.hasNext()) aIter.next();
            aIter.add(s);
        }*/
        System.out.println(a);

        //remove every second word from b
        bIter = b.iterator();
        while (bIter.hasNext()) {
            bIter.next();
            if (bIter.hasNext()) {
                bIter.next();
                bIter.remove();
            }
        }
        System.out.println(b);

        //bulk operation: remove all words in b from a
        a.removeAll(b);
        System.out.println(a);
    }
}
/*
Connected to the target VM, address: '127.0.0.1:54324', transport: 'socket'
[Amy, Bob, Carl, Doug, Erica, Frances, Gloria]
[Bob, Frances]
[Amy, Carl, Doug, Erica, Gloria]
Disconnected from the target VM, address: '127.0.0.1:54324', transport: 'socket'
*/

数组列表

  • ArrayList封装了一个动态再分配的对象数组
  • Vector类创建动态数组,其所有方法都是同步的,如果只是一个线程的话用ArrayList

散列集

  • 散列表用链表数组实现
  • 每个列表被称为桶,计算散列码,与桶的总数取余,得到的结果就是保存这个元素桶的索引
  • 装填因子(一般0.75)确定何时对于这个表进行再散列

树集

  • 树集是一个有序集合
  • 任意顺序插入,但是遍历时,值按照排序过后的方式呈现
package chapter9_set.treeSet;


import java.util.*;

public class TreeSetTest {

    public static void main(String[] args) {
        var parts = new TreeSet<Item>();
        parts.add(new Item("Toaster", 1234));
        parts.add(new Item("Widget", 4562));
        parts.add(new Item("Modem", 9912));
        System.out.println(parts);

        var sortByDescription = new TreeSet<Item>(Comparator.comparing(Item::getDescription));

        sortByDescription.addAll(parts);
        System.out.println(sortByDescription);
    }
}

package chapter9_set.treeSet;

import java.util.*;

public class Item implements Comparable<Item>
{
    private String description;
    private int partNumber;

    public Item(String aDescription, int aPartNumber) {
        description = aDescription;
        partNumber = aPartNumber;
    }

    public String getDescription() {
        return description;
    }

    public String toString() {
        return "[description = " + description + ", partNumber = " +  partNumber + " ]";
    }

    public boolean equals(Object otherObject) {
        if (this == otherObject) return true;
        if(otherObject == null) return  false;
        if(getClass() != otherObject.getClass()) return false;
        var other = (Item) otherObject;
        return Objects.equals(description, other.description) && partNumber == other.partNumber;
    }

    public int hasCode() {
        return Objects.hash(description, partNumber);
    }

    public int compareTo(Item other) {
        int diff = Integer.compare(partNumber, other.partNumber);
        return diff != 0 ? diff : description.compareTo(other.description);
    }
}

优先队列

  • 优先队列中元素可以按照任意顺序插入,但会按照有序顺序检索
  • 只要调用remove方法,总会获得当前优先队列中最小的元素
  • 优先队列使用堆,其添加和删除操作将最小元素移动到根
  • 这里迭代并没有按照顺序来访问元素,删除操作却总是删除剩余元素中最小的那个
package chapter9_set.priorityQueue_Heap;

import java.util.*;
import java.time.*;

public class PriorityQueueTest {
    public static void main(String[] args) {
        var pq = new PriorityQueue<LocalDate>();
        pq.add(LocalDate.of(1906, 12, 9));
        pq.add(LocalDate.of(1806, 12, 9));
        pq.add(LocalDate.of(1206, 12, 9));
        pq.add(LocalDate.of(1826, 12, 9));
        pq.add(LocalDate.of(1826, 2, 9));
        pq.add(LocalDate.of(1826, 1, 10));

        System.out.println("Iterating over elements...");
        for (LocalDate date : pq) {
            System.out.println(date);
        }
        System.out.println("Removing elements...");
        while (!pq.isEmpty())
            System.out.println(pq.remove());
    }
}

映射

  • map映射存放键值对

映射的基本操作

  • HashMao, TreeMap: 散列映射稍快,不需要按照有序顺序访问键选择散列映射
  • 键必须唯一
  • remove删除给定键对应的元素, size方法返回映射中元素数
  • 迭代处理映射的键和值,使用forEach方法
Map<String, Integer>scores = ...;
int score = scores.getOrDeaufault(id, 0);

scores.forEach((k. v) -> System.out.println("key" + k + ", value=" + v)); //lambda
package chapter9_set.map;

import java.util.HashMap;

public class MapTest {

    public static void main(String[] args) {
        var staff = new HashMap<String, Employee>();
        staff.put("123-15-1555", new Employee("Amy Lee"));
        staff.put("157-58-9999", new Employee("Harry Hacker"));
        staff.put("154-87-9999", new Employee("Ou Kunnan"));
        staff.put("548-84-1111", new Employee("Da Bendan"));

        System.out.println(staff.toString());
        //System.out.println(staff);

        staff.remove("123-15-1555");

        staff.put("999-99-9999", new Employee("Hee"));

        System.out.println(staff.get("999-99-9999"));

        staff.forEach((key, value) -> System.out.println("key=" + key + ", values=" + value));
    }
}

package chapter9_set.map;

public class Employee {

    private String name;

    public Employee(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

更新映射条目

counts.put(word, counts.get(word) + 1);
//第一次看到word会出现`NullPointerException`

counts.put(words, counts.getOrDefault(word, 0) + 1);

counts.putIfAbsent(word, 0);
counts.put(word, counts.get(word) + 1);

counts.merge(word, 1, Integer::sum);

映射视图

  • 3种视图: 键集, 值集, 键值集

Set keySet();

Collection values();

Set<Map, Entry<K, V>> entrySet();

  • 枚举映射条目:
for(Map.Entry<String, Employee> entry : staff.entrySet()){
    //for(var entry : staff.entrySet())
    String k = entry.getKey();
    Employee v = entry.getValue();
    ......
}
  • 在键集视图可以调用remove,会删除键值对, 但是不能添加元素(会抛出UnsupportedOperationException)
  • 映射条目集视图也有同样的限制

弱散列试图

  • WeakHashMap使用弱引用保存键
  • P400

链接散列集与映射

  • LinkedHashSet, LinkedHashMap会记住元素插入顺序
var staff = new LinkedHashMap<String, Employee>();
staff.put("153156", new Employee("BJv"));
staff.put("2516256", new Employee("Dv"));
staff.put("5453156", new Employee("AJv"));
staff.put("953156", new Employee("BAJVJv"));

var it = staff.keySet().iterator();
while(it.hasNext()){
    System.out.println(it.next());
}
//        staff.values().iterator()
  • 最近最少使用原则: 访问频率高的放在内存种,访问频率低的放在数据库
var cache = new LinkedHashMap<K, V>(128, 0.75F, true){
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest){
        return size() > 100;
    }
}

枚举集与映射

  • EnumSet没有公共构造器,使用静态工厂方法构造这个集:
enum Weekday{MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY};
EnumSet<Weekday> a = EnumSet.allOf(Weekday.class);
...
  • EnumMap是一个键类型为枚举类型的映射
var a = new EnumMap<Weekday, Employee>(Weekday.class);

标识散列映射

  • IdenetityHashMap: 键值不是使用函数hadCode, 而是方法System.identityHashCode
  • 根据内存地址计算散列码
  • 两个对象进行比较时,使用==, 而不使用equals

视图与包装器

  • keySet方法返回了一个实现了Set接口的类对象,由这个类的方法操纵原映射.这种集合称为视图
  • 视图: 根据用户观点所定义的数据结构

小集合

List<String> names = List.of("Peter", "Paul", "Mary");
Set<Integer> numbers = Set.of(2, 3, 5);

Map<String, Integer> scores = Map.of("Peter", 2, "Paul", 3, "Mary", 5);
  • 以上集合不可更改,需要更改的话
var names = new ArrayList<>(List.of("Peter", "Paul", "Mary"));

子范围

  • 可以为很多集合建立子范围视图
List<Employee> group2 = staff.subList(10, 20); //10~19

group2.clear(); // 元素会自动的从staff中删除

SortedSet<E> subSet(E from, E to);// 返回大于from, 小于to的所有元素构成的子集

不可修改视图

  • 这些视图对现有的集合增加了一个运行检查.如果发现试图对集合修改,抛出异常
  • P406

同步视图

  • Collections类的静态方法synchronizedMap方法可以将任何一个映射转换成有同步访问方法的Map
var map = Collections.synchronizedMap(new HashMaP<String, Employee>());

算法

为什么使用泛型算法

  • 数组中最大元素:
if(a.length == 0) throw new NoSuchElementException();
T largest = a[0];

for (int i = 0; i < a.length; i++) {
    if (largest.compareTo(a[i]) < 0) {
        largest = a[i];
    }
}
  • 数组列表中最大元素
if(a.size() == 0) throw new NoSuchElementException();
T largest = a.get(0);

for (int i = 0; i < a.size(); i++) {
    if (largest.compareTo(a.get(i)) < 0) {
        largest = a.get(i);
    }
}
  • 链表没有高效随机访问操作,可以使用迭代器
if (l.isEmpty())  throw new NoSuchElementException();
Iterator<T> it = l.iterator();
T largest = it.next();
while (it.hasNext()) {
    T next = it.next();
    if (largest.compareTo(next) < 0) {
        largest = next;
    }
}
  • 可以使用max方法实现能够接受任何实现了Collection接口的对象
public static <T extends Comparable> T max(Collection<T> collection) {
    if (collection.isEmpty()) throw new NoSuchElementException();
    Iterator<T> iter = collection.iterator();
    T largest = iter.next();
    while (iter.hasNext()) {
        T next = iter.next();
        if (largest.compareTo(next) < 0) {
            largest = next;
        }
    }
    return largest;
}

排序与混排

  • sort方法对实现了List接口的集合排序

Colletion.sort(staff);

  • 列表元素实现了Comparable接口
staff.sort(Comparator.comparingDouble(Employee::getSalary))
  • 逆序Collection.reverseOrder()
  • staff.sort(Comparator.comparingDouble(Employee::getSalary).reversed())
  • 列表是可修改的支持set方法,可改变大小的支持add, remove方法
package chapter9_set.shuffle;

import java.util.*;

public class ShuffleTest {

    public static void main(String[] args) {
        var numbers = new ArrayList<Integer>();

        for (int i = 1; i < 49; i++) {
            numbers.add(i);
        }
        Collections.shuffle(numbers);
        List<Integer> winningCombination = numbers.subList(0, 10);
        Collections.sort(winningCombination);

        System.out.println(winningCombination);
    }
}

二分查找

  • CollectionbinarySearch方法实现了二分查找算法,要求集合必须有序
  • 集合必须实现List接口, 如果没有采用Comparable接口的CompareTo方法进行排序,那么需要提供一个比较器对象
i = Collection.binarySearch(c, element, comparator);

返回非负值:匹配对象的索引; 返回负值, 插入位置为-i-1

  • 如果提供的是链表则退化为线性查找

简单算法

  • P416

批操作

l1.retainAll(l2);
l1.removeAll(l2);
  • 键集是映射的一个视图,staff.keySet().removeAll(terminatedIDs);键和相关的员工名字会自动的从映射中删除

集合和数组的转换

  • 数组转换为集合:
String[] names = ;
var name = new HashSet<>(List.of(names));
  • 集合得到数组使用toArray方法,返回对象数组
String[] values = staff.toArray(new String[0));

遗留的集合

  • Stack类,有pop, push方法, 扩展了Vector
  • peek返回栈顶元素

位集

  • BitSet: 高效存储位序列

//计算前两百万位素数数量 计数使用时间
package chapter9_set.sieve;

import java.util.*;

public class Sieve {

    public static void main(String[] args) {
        int n = 2000000;
        long start = System.currentTimeMillis();
        int i;
        int count = 0;
        var bitSet = new BitSet(n + 1);

        for (i = 2; i <= n; i++)
            bitSet.set(i);

        
        i = 2;
        while (i * i <= n) {
            if (bitSet.get(i)) {
                count++;
                int k = 2 * i;
                while (k <= n) {
                    bitSet.clear(k);
                    k += i;
                }
            }
            i++;
        }
        // i = sqrt(n);
        while (i <= n) {
            if (bitSet.get(i))
                count++;
            i++;
        }
        long end = System.currentTimeMillis();

        System.out.println("From 2~2000000, there are " + count  + " primes.");
        System.out.println("Time consumed:" + (end - start));
    }
}