动态数组和链表的复杂度分析.

1.动态数组

这篇文章分析的复杂度我会从增删改查四个操作入手 其实一般也是从这四个角度去分析

1.add(int index, E element)

这个方法中 主要的操作是挪动元素和数组扩容

1.最好复杂度

数组的添加方法中的最好情况就是尾插并且没有进行数组扩容操作 尾插不需要挪动任何元素 次数为1 所以这种情况对应的时间复杂度是O(1)

2.最坏复杂度

数组的添加方法中的最坏情况就是头插并且考虑到了数组扩容操作 头插相当于需要挪动n个元素 这篇文章中所说的n是数组的规模 数组扩容操作也是需要挪动n个元素 相当于总的操作次数是2n 经过简化 这种情况对应的时间复杂度是O(n)

3.平均复杂度

计算平均复杂度所需的参数是每个位置插入操作对应的次数 对他们求和得到求和结果 然后对求和结果进行均分操作即可得到最终结果
从最好情况到最坏情况的每一种情况中 其实需要考虑数组扩容的频率很低 事实证明确实如此 如果我们每种情况都不去考虑数组扩容的操作 只考虑挪动元素的操作的话 那么从最好到最坏的情况对应的次数依次是1 2 3……n 然后再加上过程中低频率的数组扩容情况 那么得到的最终平均复杂度是(1 + 2 + …… + n + 2 x n) / n = [n ^ 2 / 2 + 3 / 2 x n] / n = n / 2 + 3 /2(假设扩容次数为2) 经过简化 得到的最终结果是O(n) 所以说其实你每一种情况考不考虑数组扩容操作其实对这三种复杂度的结果没有影响

2.remove(int index)

这个方法中 主要的操作是挪动元素

1.最好复杂度

最好情况是尾删 对应的时间复杂度是O(1)

2.最坏复杂度

最坏情况是头删 需要挪动数组规模的元素 对应的时间复杂度是O(n)

3.平均复杂度

从最好情况到最坏情况的每一种情况 执行次数依次是1 2 3 …… n 所以说最终的平均复杂度是 (1+ 2 + …… + n) / n = n / 2 + 1 / 2 所以经过简化后得到的结果是O(n)

3.set(int index, E element)

在了解该方法的相关复杂度之前 我们首先要来了解一下数组访问的方式 可能很多人会以为数组访问是通过从前往后遍历的方式实现的 但事实并非如此 他取决于首地址和目标元素的偏移量 根据这两个参数获取目标元素的地址值 按照地址值访问到指定元素 这个操作的复杂度是O(1) 重置操作其实就是在访问操作的基础上对访问到的元素进行更新操作

1.最好、最坏以及平均复杂度

既然访问数组中任意位置的元素的复杂度是O(1)的话 那么重置数组中任意位置的元素的时间复杂度其实也是O(1) 并且没有分最好情况和最坏情况 所以对于该操作而言 最好、最坏以及平均复杂度其实都是O(1)

4.get(int index)

在了解完重置方法以后 其实查询方法就很容易理解了 既然重置方法是在访问数组元素的基础上对指定元素进行更新操作的话 那么查询就直接是访问数组元素

1.最好、最坏以及平均复杂度

了解了get方法的本质 其实要得出他的相关复杂度就容易多了 他的三个相关复杂度其实都是O(1)

2.链表

1.add(int index, E element)

在这个方法中影响复杂度的操作主要是node方法中的查找待插入节点的前置节点的遍历操作 和数组不一样的是 他并没有扩容操作

1.最好复杂度

最好情况就是头插 无需遍历 对应的时间复杂度是O(1)

2.最坏复杂度

最坏情况就是尾插 需要遍历的元素个数是n 对应的时间复杂度是O(n)

3.平均复杂度

从最好情况到最坏情况过程中的所有情况对应的操作次数应该是1 2 3 …… n 那么总的操作次数是n ^ 2 / 2 + n / 2 取平均值以后的结果为n / 2 + 1 / 2 经过简化以后的最终结果就是平均复杂度 即为O(n)

2.remove(int index)

删除的总体逻辑是先找到待删除节点的前置节点 然后让该前置节点指向待删除节点的后置节点 这个过程中可知删除方法的操作次数主要取决于找寻前置节点的次数

1.最好复杂度

最好情况就是头删 对应的时间复杂度是O(1)

2.最坏复杂度

最坏情况是尾删 对应的时间复杂度是O(n)

3.平均复杂度

从最好情况到最坏情况这个过程中 所有情况对应的是时间复杂度依次为1 2 3 …… n 所以最终的平均复杂度应为O(n) 这里的计算过程其实在刚才的add方法中就有所体现 这里就不展开论述了

4.一些关于链表增删的说法

有的人其实会认为链表的增删操作对应的时间复杂度是O(1) 其实这个是根据你看的角度 如果你想要看的只是增加节点或者删除节点这一个单独的操作 那么复杂度确实是O(1) 但是在增加或删除方法中 不值只有增加或者删除节点的操作 还包含了寻找待删除节点的前置节点这一操作 所以说如果你需要考虑这个地方的话 那么是时间复杂度必然是O(n)

3.set(int index, E element)

与数组的访问不一样的是 链表的访问不能直接定位到指定位置 而是需要从头到尾依次遍历 直到定位到指定节点

1.最好复杂度

最好情况其实就是头部重置 无需遍历 对应的是时间复杂度是O(1)

2.最坏复杂度

最坏情况是尾部重置 需要遍历数组规模次 对应的时间复杂度是O(n)

3.平均复杂度

其实就是求和 求平均值 刚才的很多地方都有类似的过程 这里就不详细阐述了 对应的复杂度是O(n)

4.get(int index)

其实他和set的复杂度是一样的 因为他们执行的操作十分类似

1.最好复杂度

最好情况是头部查询 对应的复杂度是O(1)

2.最坏复杂度

最坏情况是尾部查询 对应的复杂度是O(n)

3.平均复杂度

求和 求平均值 对应的复杂度是O(n)

3.均摊复杂度

在动态数组中其实还有一个方法的复杂度我们没有分析 即add(E element) 往数组尾部添加指定元素 add方法的操作次数主要取决数组扩容的次数 但是这个操作的频率很低
我们就以这个方法讲一下均摊复杂度

1.最好复杂度

在讲均摊行为之前 首先还是得分析一下老三件套 由于这个方法限定了插入的位置为尾部 所以需要考虑的地方只剩下了数组是否要进行扩容 最好情况就是不需要进行扩容 对应的复杂度就是O(1)

2.最坏复杂度

和最好情况相反 最坏情况就是考虑了数组扩容以后的产物 即O(n) 这是因为扩容遍历了数组规模的元素

3.平均复杂度

由于在所有情况中数组扩容的频率低 所以其实对应的复杂度就是O(1)

4.均摊复杂度

我可以以一个例子简单说明一下:
比如现在有一个元素个数为1的数组 他的容量为4 前三种情况对应的操作次数都为1 第四种情况需要进行数组扩容操作 所以需要n次 外加当前位置上的的添加操作1次
面对这种情况 其实我们可以讲第四次操作中的扩容操作次数分摊到这四次的add操作中 那么其实相当于说每次的add次数都为2 那么对应的复杂度其实就是(2 + 2 + 2 + 2) / n = 2 x n / n = 2 简化以后对应的均摊复杂度为O(1)

5.均摊复杂度的使用场景

均摊复杂度的适用于一些经历连续多次的较低复杂度之后突然出现个别复杂度较高的情况

4.复杂度震荡

我们借由数组的缩容操作来讲解一下这个概念
我们肯定会遇到这么一种情况 就是内存使用很紧张 但是动态数组中的内存依然比较多 所以我们可以通过缩容操作来将动态数组中的内存分配给其他用途 缩容操作主要的对象就是删除方法

1.缩容方法的实现

public void trim(){
	// 首先获取数组的旧容量
	int oldCapacity = elements.length;
	// 接着我们设计数组的新容量为旧容量的1/2
	int newCapacity = oldCapacity >> 1;
	// 然后排除掉不需要缩容的情况 即设计缩容时机 DEFAULT_CAPACITY限制了数组的最小容量
	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;
	System.out.println(oldCapacity + "缩容为" + newCapacity);
}

2.缩容方法和扩容方法的时机

缩容方法是在删除元素以后执行的 而扩容方法是在添加元素之前进行的 他们在各自方法中的体现分别如下所示:

public void add(int index, E element){
        // 对缩影进行索引越界检查
        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){
        rangeCheck(index);
        E delete = elements[index];
        for (int i = index; i < size; i++) {
            elements[i] = elements[i + 1];
        }
        size--;
        elements[size] = null;
        trim();
        return delete;
    }

3.复杂度震荡

比如现在我们有一个容量为4的动态数组 里面存有2个元素 其中扩容的倍数为2 缩容时机的倍数为1/2 两者相乘为1
现在要对其进行添加操作
第一次添加的复杂度为O(1)
第二次添加的复杂度为O(1)
第三次添加的复杂度为O(n) 这次和前两次不同 这次操作涉及到了数组的扩容操作 所以说复杂度变成了O(n)
接着我们的第四次操作进行的是删除操作 对应的复杂度是O(n)
接着我们重复添加删除添加删除操作 发现接下去的复杂度都是O(n)
现在我们其实已经可以描述出复杂度震荡的大致表现 就是经历一段连续的较低复杂度以后 突然复杂度变成了较高的水平 并且保持了一段时间
那么复杂度震荡的条件其实是扩容倍数以及缩容时机倍数设置的不合理 具体就是这两个倍数的乘积为1 导致触发了复杂度震荡的现象 所以我们只需要将倍数乘积改为非1的数据即可
比如现在我们可以将缩容时机改成当数组元素个数达到容量的1/4的时候 数组的容量缩容为原来容量的1/2 这样的话 扩容的倍数和缩容时机的倍数2 * 1/4就不等于1了 也就不会出现刚才复杂度震荡的现象了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

axihaihai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值