四、复杂度分析& 动态数组的缩容

242 阅读3分钟

复杂度分析

这里分析之前实现的ArrayList和LinkedList的增删改查的复杂度。 分析复杂度是要从下面三个方面分析 1. 最好情况复杂度 2. 最坏情况复杂度 3. 平均情况复杂度

ArrayList的复杂度分析

public E get(int index) {// O(1)
    rangeCheck(index);
    return elements[index];
}
public E set(int index, E element) {// O(1)
    rangeCheck(index);
    
    E old = elements[index];
    elements[index] = element;
    return old;
}
public void add(int index, E element) {
    /**
    * 最好:O(1)
    * 最坏:O(n)
    * 平均:(1+2+...+n)/n = n/2 ==> O(n)
    */
    rangeCheckForAdd(index);
    ensureCapacity(size + 1);
    
    for (int i = size; i > index; i--) {
    	elements[i] = elements[i - 1];
    }
    elements[index] = element;
    size++;
}
public E remove(int index) {
    /**
    * 最好:O(1)
    * 最坏:O(n)
    * 平均:(1+2+...+n)/n = n/2 ==> O(n)
    */
    rangeCheck(index);
    
    E old = elements[index];
    for (int i = index + 1; i < size; i++) {
    	elements[i - 1] = elements[i];
    }
    elements[--size] = null;
    return old;
}

LinkedList的复杂度分析

public E get(int index) {
    //最好:O(1)、最坏:O(n)、平均:O(n)
    return findNode(index).element;
}
public E set(int index, E element) {
    //最好:O(1)、最坏:O(n)、平均:O(n)
    Node<E> node = findNode(index);
    E old = node.element;
    node.element = element;
    return old;
}
public void add(int index, E element) {
    //最好:O(1)、最坏:O(n)、平均:O(n)
    rangeCheckForAdd(index);
    if(index == 0) {
    	first = new Node<E>(element, first);
    }else {
    	Node<E> prev = findNode(index - 1);
    	prev.next =new Node<E>(element, prev.next);
    }
    size++;
}
public E remove(int index) {
    //最好:O(1)、最坏:O(n)、平均:O(n)
    rangeCheck(index);
    Node<E> node = first;
    if(index == 0) {
    	first = first.next;
    }else {
    	Node<E> prev = findNode(index - 1);
    	node = prev.next;
    	prev.next = node.next;
    }
    size--;
    return node.element;
}

动态数组、链表复杂度分析

从上面表格中可以看出动态数组的平均复杂度比链表的平均复杂度要好些,那么为啥会有人说链表的增删复杂度是O(1)比动态数组的要好呢?那是因为这种说法是在增删的那一刻,但是从整个增删改查的方法来看还是动态数组的比较好,但是链表最大的特点是省内存。

动态数组add(E element)复杂度分析

因为add(E element)方法都是在数组的最后添加元素,所有一般复杂度都是O(1),但是当数组容量不够时,会进行扩容,那么这一次扩容就是O(n),扩容比较少,所有这里需要使用均摊复杂:就是把一次库容的复杂度均摊到其他操作中,如上图。

什么情况下适合使用均摊复杂度 经过连续的多次复杂度比较低的情况后,出现个别复杂度比较高的情况

动态数组的缩容

如果内存使用比较紧张,动态数组有比较多的剩余空间,可以考虑进行缩容操作

比如剩余空间占总容量的一半时,就进行缩容

public E remove(int index) {

	rangeCheck(index);
	
	E old = elements[index];
	for (int i = index + 1; i < size; i++) {
		elements[i - 1] = elements[i];
	}
	elements[--size] = null;
	
	trim();
	return old;
}

//缩容
private void trim() {
	int oldCapacity = elements.length;
	int newCapacity = oldCapacity >>1;
	if(size >= newCapacity || oldCapacity <= DEFAULT_CAPACITY) return;
	
	// 剩余空间还很多
	E[] newElements = (E[]) new Object[newCapacity];
	for(int i = 0;i < size;i++) {
		newElements[i] = elements[i];
	}
	elements = newElements;
}

动态数组 - clear方法的缩容

public void clear() {
	for (int i = 0; i < size; i++) {
		elements[i] = null;
	}
	size = 0;
	if(elements != null && elements.length > DEFAULT_CAPACITY) {
		elements = (E[]) new Object[DEFAULT_CAPACITY];
	}
}

如果扩容倍数、缩容时机设计不得当,有可能会导致复杂度震荡

复杂度震荡: 如果我们在扩容时设计的是容量是2倍,缩容设计的是旧容量的一半,此时如果我们在容量满的时候插入一个数据,会导致扩容,然后在删除一个元素,又会发生缩容,这样的复杂度就是O(n), 就会出现复杂度震荡

只要扩容和缩容的乘等于1就会出现复杂度震荡。\color{red}{只要扩容和缩容的乘等于1就会出现复杂度震荡。}

例如下图:将之前的扩容1.5倍改成2倍,缩容是1/2倍,那么他们相乘就等于1,因此就会出现复杂度震荡