玩转数据结构笔记(2)

249 阅读4分钟

数组

当我们不停的像数组添加元素和删除元素,就会面临一个问题,数组的容量是否足够或太大。容量太大,很有可能浪费空间,容量不够也会导致元素无法添加。我们需要一个动态容量的数组。

动态数组

动态数组是指容量自动变化的数组,不是在初始化数组时可以定义为容量动态。所以需要在添加或者删除的时候,进行一些容量的判断从而执行更改数组容量的命令。如,添加元素时,发现容量不够了,那么扩容,删除元素时发现数组容量过大(如何定义会影响性能),就需要缩容。

扩容

Java中的Collection的扩容,是将数组的容量扩大到当前容量的1.5倍,这个倍数过大或太小都会影响性能,也不能选取某个常数。

缩容

当删除元素执行了很多次,数组元素总量小于数组容量时,需要缩容来节省空间。

扩容或缩容的实现

重新初始化一个新容量的数组,将之前数组遍历放到新的数组中,所以时间复杂度为O(n)

简单复杂度

数组的各种操作复杂度为:

添加元素: 添加到最后O(1),添加到最前O(n),平均下来是 O(n/2) -> O(n),还要加上偶尔会执行的扩容操作O(n),总体来说 是 O(n)

删除元素:删除最后O(1),删除最前O(n),平均下来是 O(n/2) -> O(n),还要加上偶尔会执行的缩容操作O(n),总体来说 是 O(n)

修改元素:知道下标O(1),否则O(n)

查找:知道下标O(1),否则O(n)

均摊复杂度

简单拿 添加元素到数组最后一个的操作(addLast)来说:

当前有一个容量为8的数组,连续添加元素(addLast)8次后,我再添加元素(addLast)时,需要先扩容,再添加(addLast)。

执行9次addLast总共执行 8(8次添加元素) + 8(扩容遍历8次) + 1(添加第9个元素) = 17次基本操作

当前有一个容量为n的数组,连续添加元素(addLast)n次后,我再添加元素(addLast)时,需要先扩容,再添加。

执行n+1次addLast总共执行 n(n次添加元素) + n(扩容遍历n次) + 1(添加第n+1个元素) = 2n +1次基本操作

这样平摊下来,addLast 的平均基本操作为 2n+1 / n+1 = 2 - 1/n+1 -> 2,

不受n影响 均摊复杂度也就是 O(1)

同理 删除最后一个元素的操作(removeLast)的均摊复杂度也为O(1)

复杂度的震荡

现在出现一个问题,假设我们设置数组缩容的条件是:

数组的元素数量小于等于数组的容量的一半。

那么:

一个容量为10的数组,存放了10个元素,我们addLast (时间复杂度为O(n),因为要扩容),addLast之后,我们removeLast(恰好扩容后数组容量为20,删除元素后,数组元素数量为10,小于等于数组容量的一半,需要缩容,时间复杂度为O(n)),

再addLast,再RemoveLast,...

发现了问题,我们本来觉得应该是均摊复杂度为O(1)的2个操作,在特定场景下,会使复杂度提高,这叫做复杂度的震荡。

如何优化复杂度的震荡呢?

我们发现,我们删除元素后,数组的元素数量小于等于数组的容量的一半就缩容了,这个缩容的条件有点问题,因为我们并不知道删除元素操作执行完以后是否还会继续执行删除元素操作,或者是添加元素操作,也就是说,我们不知道是否有缩容的必要,那么如何判断是否有缩容的必要呢?我们把条件变得苛刻一点,当你一直删除元素,把数组的元素数量删除到只有数组容量的1/4,这个时候,数组的容量是元素数量的4倍,那么是时候缩容了。