Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
think-dast-zh
提交
b3318859
T
think-dast-zh
项目概览
OpenDocCN
/
think-dast-zh
8 个月 前同步成功
通知
0
Star
26
Fork
13
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
T
think-dast-zh
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
提交
b3318859
编写于
9月 03, 2017
作者:
W
wizardforcel
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
ch3.
上级
bbf16128
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
146 addition
and
0 deletion
+146
-0
3.md
3.md
+146
-0
未找到文件。
3.md
0 → 100644
浏览文件 @
b3318859
# 第三章 `ArrayList`
本章一举两得:我展示了上一个练习的解法,并展示了一种使用摊销分析来划分算法的方法。
## 3.1 划分`MyArrayList`的方法
对于许多方法,我们不能通过测试代码来确定增长级别。例如,这里是
`MyArrayList`
的
`get`
的实现:
```
java
public
E
get
(
int
index
)
{
if
(
index
<
0
||
index
>=
size
)
{
throw
new
IndexOutOfBoundsException
();
}
return
array
[
index
];
}
```
`get`
中的每个东西都是常数时间的。所以
`get`
是常数时间,没问题。
现在我们已经划分了
`get`
,我们可以使用它来划分
`set`
。这是我们以前的练习中的
`set`
:
```
java
public
E
set
(
int
index
,
E
element
)
{
E
old
=
get
(
index
);
array
[
index
]
=
element
;
return
old
;
}
```
该解决方案的一个有些机智的部分是,它不会显式检查数组的边界;它利用
`get`
,如果索引无效则引发异常。
`set`
中的一切,包括
`get`
的调用都是常数时间,所以
`set`
也是常数时间。
接下来我们来看一些线性的方法。例如,以下是我的实现
`indexOf`
:
```
java
public
int
indexOf
(
Object
target
)
{
for
(
int
i
=
0
;
i
<
size
;
i
++)
{
if
(
equals
(
target
,
array
[
i
]))
{
return
i
;
}
}
return
-
1
;
}
```
每次在循环中,
`indexOf`
调用
`equals`
,所以我们首先要划分
`equals`
。这里就是:
```
java
private
boolean
equals
(
Object
target
,
Object
element
)
{
if
(
target
==
null
)
{
return
element
==
null
;
}
else
{
return
target
.
equals
(
element
);
}
}
```
此方法调用
`target.equals`
;这个方法的运行时间可能取决于
`target`
或
`element`
的大小,但它不依赖于该数组的大小,所以出于分析
`indexOf`
的目的,我们认为这是常数时间。
回到之前的
`indexOf`
,循环中的一切都是常数时间,所以我们必须考虑的下一个问题是:循环执行多少次?
如果我们幸运,我们可能会立即找到目标对象,并在测试一个元素后返回。如果我们不幸,我们可能需要测试所有的元素。平均来说,我们预计测试一半的元素,所以这种方法被认为是线性的(除了在不太可能的情况下,我们知道目标元素在数组的开头)。
`remove`
的分析也类似。这里是我的时间。
```
java
public
E
remove
(
int
index
)
{
E
element
=
get
(
index
);
for
(
int
i
=
index
;
i
<
size
-
1
;
i
++)
{
array
[
i
]
=
array
[
i
+
1
];
}
size
--;
return
element
;
}
```
它使用
`get`
,这是常数时间,然后从
`index`
开始遍历数组。如果我们删除列表末尾的元素,循环永远不会运行,这个方法是常数时间。如果我们删除第一个元素,我们遍历所有剩下的元素,它们是线性的。因此,这种方法同样被认为是线性的(除了在特殊情况下,我们知道元素在末尾,或到末尾距离恒定)。
## 3.2 `add`的划分
这里是
`add`
的一个版本,接受下标和元素作为参数:
```
java
public
void
add
(
int
index
,
E
element
)
{
if
(
index
<
0
||
index
>
size
)
{
throw
new
IndexOutOfBoundsException
();
}
// add the element to get the resizing
add
(
element
);
// shift the other elements
for
(
int
i
=
size
-
1
;
i
>
index
;
i
--)
{
array
[
i
]
=
array
[
i
-
1
];
}
// put the new one in the right place
array
[
index
]
=
element
;
}
```
这个双参数的版本,叫做
`add(int, E)`
,它使用了单参数的版本,称为
`add(E)`
,它将新的元素放在最后。然后它将其他元素向右移动,并将新元素放在正确的位置。
在我们可以划分双参数
`add`
之前,我们必须划分单参数
`add`
:
```
java
public
boolean
add
(
E
element
)
{
if
(
size
>=
array
.
length
)
{
// make a bigger array and copy over the elements
E
[]
bigger
=
(
E
[])
new
Object
[
array
.
length
*
2
];
System
.
arraycopy
(
array
,
0
,
bigger
,
0
,
array
.
length
);
array
=
bigger
;
}
array
[
size
]
=
element
;
size
++;
return
true
;
}
```
单参数版本很难分析。如果数组中存在未使用的空间,那么它是常数时间,但如果我们必须调整数组的大小,它是线性的,因为
`System.arraycopy`
所需的时间与数组的大小成正比。
那么
`add`
是常数还是线性时间的?我们可以通过考虑一系列
`n`
个添加中,每次添加的平均操作次数,来分类此方法。为了简单起见,假设我们以一个有
`2`
个元素的空间的数组开始。
+
我们第一次调用
`add`
时,它会在数组中找到未使用的空间,所以它存储
`1`
个元素。
+
第二次,它在数组中找到未使用的空间,所以它存储
`1`
个元素。
+
第三次,我们必须调整数组的大小,复制
`2`
个元素,并存储
`1`
个元素。现在数组的大小是
`4`
。
+
第四次存储
`1`
个元素。
+
第五次调整数组的大小,复制
`4`
个元素,并存储
`1`
个元素。现在数组的大小是
`8`
。
+
接下来的
`3`
个添加储存
`3`
个元素。
+
下一个添加复制
`8`
个并存储
`1`
个。现在的大小是
`16`
。
+
接下来的
`7`
个添加复制了
`7`
个元素。
以此类推,总结一下:
+
`4`
次添加之后,我们储存了
`4`
个元素,并复制了两个。
+
`8`
次添加之后,我们储存了
`8`
个元素,并复制了
`6`
个。
+
`16`
次添加之后,我们储存了
`16`
个元素,并复制了
`14`
个。
现在你应该看到了规律:要执行
`n`
次添加,我们必须存储
`n`
个元素并复制
`n-2`
个。所以操作总数为
`n + n - 2`
,为
`2 * n - 2`
。
为了得到每个添加的平均操作次数,我们将总和除以
`n`
;结果是
`2 - 2 / n`
。随着
`n`
变大,第二项
`2 / n`
变小。参考我们只关心
`n`
的最大指数的原则,我们可以认为
`add`
是常数时间的。
有时线性的算法平均可能是常数时间,这似乎是奇怪的。关键是我们每次调整大小时都加倍了数组的长度。这限制了每个元素被复制的次数。否则 - 如果我们向数组的长度添加一个固定的数量,而不是乘以一个固定的数量 - 分析就不起作用。
这种划分算法的方式,通过计算一系列调用中的平均时间,称为摊销分析。你可以在
<http://thinkdast.com/amort>
上阅读更多信息。重要的想法是,复制数组的额外成本是通过一系列调用展开或“摊销”的。
现在,如果
`add(E)`
是常数时间,那么
`add(int, E)`
呢?调用
`add(E)`
后,它遍历数组的一部分并移动元素。这个循环是线性的,除了在列表末尾添加的特殊情况中。因此,
`add(int, E)`
是线性的。
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录