[Logic of Java Programming] Lists and Queues

ArrayList

There are two methods in ArrayList that can return data

public Object[] toArray();
public <T> T[] toArray(T[] a);

There is a static method asList in ArrayList that can return the corresponding List

Integer[] a = {1, 2, 3};
List<Integer> list = Arrays.asList(a);

The List returned by this method is the incoming array inside, so the modification of the data will also be reflected in the List, and calling the add and remove methods on the List will throw an exception.
To use the complete method of ArrayList, you should create a new ArrayList

List<Integer> list = new ArrayList<Integer>(Arrays.asList(a));

Summary of ArrayList features:
1. It can be accessed randomly, and the access efficiency according to the index position is very high. O(1)
2. Finding elements by content is inefficient unless the array is sorted. O(N)
3. Insertion and deletion of elements is relatively inefficient. O(N)
4. Not thread safe

LinkedList

LinkedList also implements the queue interface Queue, which is characterized by first-in, first-out.

public interface Queue<E> extends Collection<E> {
    boolean add(E e);
    boolean offer(E e);
    E remove();
    E poll();
    E element();
    E peek();
}

Queue mainly extends the Collection interface, mainly for three operations:

  • Add elements at the end (add, offer)
  • View the head element (element, peek), return the head element, but do not change the queue
  • Remove the head element (remove, poll), return the head element, and remove from the queue
    Each operation corresponds to two methods. The difference between them is that when the queue is empty, element and remove will throw an exception, while peek and poll will return null; when the queue is full, add will throw an exception and offer returns false

LinkedList is actually a directly implemented Duque interface, which represents a double-ended queue. It can implement stack-related operations, and the stack is characterized by first-in, last-out and last-in, first-out.

public interface Deque<E> extends Queue<E> { 
    // 表示入栈,在头部添加元素,如果栈满了,会抛出异常
    void push(E e);
    // 出栈,返回头部元素,并且从栈中删除,如果栈为空,抛出异常
    E pop();
    // 查看栈头部元素,如果栈为空,返回null
    E peek();
    // 从后往前遍历
    Iterator<E> descendingIterator();
}

LinkedList and ArrayList are similar in usage, except that LinkedList adds an interface Deque, which can be regarded as a queue, stack, and double-ended queue.

LinkedList implementation principle

ArrayList is an array inside, and elements are stored continuously in memory, but LinkedList is not. The literal translation of LinkedList is a linked list. Its interior is a doubly linked list . Each element is stored separately in memory, and the elements are linked together by links.
In order to represent the link relationship, a node is needed, the node includes the actual element, and two links point to the previous node (predecessor) and the next node (successor) respectively.

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;
    }
}

The internal composition of LinkedList is mainly composed of the following three brother instance variables

int size = 0;       // 表示链表长度
Node<E> first;      // 指向头节点
Node<E> last;       // 指向尾节点

add element add

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++;
}

Unlike ArrayList, LinkedList's memory is allocated on demand, and adding elements is also very simple, just add links directly at the tail node

get element get

public E get(int index) {
    // 检查index是否超出范围
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) {
    // sieze >> 1相当于size/2
    // 如果index在size的前半部分,则从头节点开始找,否则从尾节点开始
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

Unlike ArrayList, the array elements in ArrayList are stored continuously and can be located directly according to the index, while in LinkedList, you must search along the link from the beginning or end of the node.

insert element

add is to add elements at the end. If you insert elements at the head or in the middle, you can use the following methods

public void add(int index, E element) {
    // 检查index是否合法
    checkPositionIndex(index);
    // 如果index正好等于size,那么就直接调用添加方法
    if (index == size)
        linkLast(element);
    else        
        linkBefore(element, node(index));
}
/**
 * @succ 目标位置当前的节点  这里插入后,就变成后继了
 */
void linkBefore(E e, Node<E> succ) {
    // 获取当前位置节点的前驱
    final Node<E> pred = succ.prev;
    // 新建节点,前驱是pred,后继是之前位置上的节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 后驱的前驱重新给值
    succ.prev = newNode;    
    // 前驱的后继重新给值
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    // 增加长度
    size++;
    modCount++;
}

Inserting elements in the middle, LinkedList only needs to allocate memory on demand, modify the links of predecessor and successor nodes, while ArrayList may need to allocate a lot of extra space and move subsequent elements

remove removes an element

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    // 后继元素
    final Node<E> next = x.next;
    // 前驱元素
    final Node<E> prev = x.prev;
    // 如果前驱为空,那么头节点就是后继
    if (prev == null) {
        first = next;
    } else {
        // 将前驱的后继链接到后继元素
        prev.next = next;
        x.prev = null;
    }
    // 后继同理前驱
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }
    // 大小改变
    x.item = null;
    size--;
    modCount++;
    return element;
}

To delete the x node, the basic idea is to link the predecessor and successor of x.

LinkedList Features

  1. Allocate space on demand, no need to pre-allocate additional space
  2. It cannot be accessed randomly. The access efficiency according to the index position is relatively low. It must be found along the link from the beginning or the end, and the efficiency is O(N/2)
  3. Regardless of whether the list is sorted, as long as the elements are searched according to the content, the efficiency is relatively low, and they must be compared one by one, and the efficiency is O(N)
  4. Adding and removing elements at both ends is very efficient, O(1)
  5. Inserting and deleting elements in the middle requires positioning first, which is inefficient, O(N), but the modification itself is very efficient, O(1)

LinkedList is an ideal choice if the length of the list is unknown, there are many operations to add and delete, especially from both ends, and there are relatively few accesses according to the index position.

ArrayDeque

ArrayDeque is a double-ended queue implemented based on arrays. It mainly has the following properties

// 存储队列中节点的数组
transient Object[] elements;
// 代表头指针
transient int head;
// 代表尾指针
transient int tail;
// 代表创建一个队列的最小容量
private static final int MIN_INITIAL_CAPACITY = 8;

Let's look at the constructor again

public ArrayDeque() {
    elements = new Object[16];
}

public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

public ArrayDeque(Collection<? extends E> c) {
    allocateElements(c.size());
    addAll(c);
}

private void allocateElements(int numElements) {
    // 如果numElements小于8,那么数组长度就分配8
    intinitialCapacity = MIN_INITIAL_CAPACITY;
    if (numElements >= initialCapacity) {
        // 如果numElements大于8,分配的长度是严格大于numElements并且为2的整数次幂
        initialCapacity = numElements;
        initialCapacity |= (initialCapacity >>>  1);
        initialCapacity |= (initialCapacity >>>  2);
        initialCapacity |= (initialCapacity >>>  4);
        initialCapacity |= (initialCapacity >>>  8);
        initialCapacity |= (initialCapacity >>> 16);
        initialCapacity++;

        if (initialCapacity < 0)   // Too many elements, must back off
            initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
    }
    elements = new Object[initialCapacity];
}

If the length of explicitly passed in elements is not specified, it defaults to 16. If you explicitly pass in a variable representing the length of elements, allocateElements will be called to do some simple processing. The main processing is in the code comments above. Here it calculates the integer power of 2 strictly greater than numElements by first setting all positions of numElements in binary form, and then +1.

add add from the tail

public boolean add(E e) {
    addLast(e);
    return true;
}

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    // 将元素添加到尾指针的位置    
    elements[tail] = e;
    // 将tail指向向一个位置,如果满了,就扩容
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}

Here we focus on the statement that determines whether the current queue is full

if ( (tail = (tail + 1) & (elements.length - 1)) == head)

When we constructed the elements element before, we said that its length must be the exponential level of 2, so for any value of the exponential level of 2, after subtracting 1, its binary must be all 1, for example: 8-1 111 after that, 1111 after 16-1. For tail, when tail+1 is less than or equal to elements.length - 1, the result after the two is tail+1, but if tail+1 is greater than elements.length - 1, the result is 0 after the two are added. . It is equal to the position of the head pointer.

private void doubleCapacity() {
    assert head == tail;
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    // 分配一个长度翻倍的数组    
    Object[] a = new Object[newCapacity];
    // 将head右边的元素赋值到新数组开头
    System.arraycopy(elements, p, a, 0, r);
    // 将head左边的元素赋值到新数组中
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    // 重新设置head和tail
    head = 0;
    tail = n;
}

add addFirst from the head

public void addFirst(E e) {
    if(e == null)
        throw new NullPointerException();
    // 让head指向前一个位置    
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

pop

public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    if (result == null)
        return null;
    elements[h] = null;
    head = (h + 1) & (elements.length - 1);
    return result;
}

This method is very simple, you can directly get the head element of the array, and then move the head one position back. This is a dequeue operation. In fact, the delete operation is also a dequeue, and the pollFirst method is still called internally.

View length

public int size() {
    return (tail - head) & (elements.length - 1);
}

ArrayDeque Features

  1. The efficiency of adding and deleting elements at both ends is very high, and the memory allocation and array copying overhead required for dynamic expansion can be amortized. Specifically, the efficiency of adding N elements is O(N)
  2. The efficiency of searching and deleting according to the element content is relatively low, which is O(N)
  3. Unlike ArrayList and LinkedList, there is no concept of indexing

iterator

Iterable Iterator

Iterable means iterable, and it has a method iterator() that returns an Iterator object.

public interface Iterable<T> {
    Iterator<T> iterator();
}
public interface Iterator<E> {
    // 判断是否还有元素未访问
    boolean hasNext();
    // 返回下一个元素
    E next();
    // 删除最后返回的元素。如果没有调用过next(),直接调用remove()是会报错的
    void remove();
}

If the object implements Iterable, you can use the foreach syntax. Foreach syntax, the compiler will convert it to call the methods of the Iterable and Iterator interfaces.

ListIterator

ArrayList also provides two methods that return the Iterator interface:

// 返回的迭代器从0开始
public ListIterator<E> listIterator();
// 返回的迭代器从指定位置index开始
public ListIterator<E> listIterator(int index);

ListIterator extends the Iterator interface

public interface ListIterator<E> extends Iterator<E> {  
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();
    void set(E e);
    void add(E e);
}

Iterator FAQ

A common misuse is to call a container's delete method in the middle of an iteration:

public void remove(ArrayList<Integer> list) {
    for(Integer a : list) {
        if(a<=100) {
            list.remove(a);
        }
    }
}

Doing so will throw an exception. Because the iterator will maintain some data related to the index position, it is required that during the iteration process, the container cannot undergo structural changes (adding and deleting elements).
Using the remove method of the iterator can avoid errors

public static void remove(ArrayList<Integer> list) {
    Iterator<Integer> it = list.iterator();
    while(it.hasNext()) {
        if(it.next() < 100) {
            it.remove();
        }
    }
}

Why use the iterator's remove method?

Iterator Fundamentals

First look at the iterator method in ArrayList

public Iterator<E> iterator() {
    return new Itr();
}

Itr is a member inner class of ArrayList,

private class Itr implements Iterator<E> {
    // 下一个要返回的元素的位置
    int cursor;
    // 最后一个返回的索引位置,如果没有为-1
    int lastRet = -1;
    // 期望的修改次数
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size;
    }

    public E next() {
        // 检查是否发生了结构变化
        checkForComodification();
        // 更新cursor和lastRet
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        // 返回对应的元素
        return (E) elementData[lastRet = i];
    }
    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            // 通过ArrayList的remove方法删除
            ArrayList.this.remove(lastRet);
            // 更新cursor和lastRet
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325876088&siteId=291194637