Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
think-dast-zh
提交
6132c1e1
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 搜索 >>
提交
6132c1e1
编写于
9月 02, 2017
作者:
W
wizardforcel
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
ch2.
上级
12b0236c
变更
1
隐藏空白更改
内联
并排
Showing
1 changed file
with
123 addition
and
0 deletion
+123
-0
2.md
2.md
+123
-0
未找到文件。
2.md
0 → 100644
浏览文件 @
6132c1e1
# 第二章 算法分析
我们在前面的章节中看到,Java 提供了两种实现
`List`
的接口,
`ArrayList`
和
`LinkedList`
。对于一些应用,
`LinkedList`
更快;对于其他应用,
`ArrayList`
更快。
要确定对于特定的应用,哪一个更好,一种方法是尝试它们,并看看它们需要多长时间。这种称为“性能分析”的方法有一些问题:
+
在比较算法之前,您必须实现这两个算法。
+
结果可能取决于您使用什么样的计算机。一种算法可能在一台机器上更好;另一个可能在不同的机器上更好。
+
结果可能取决于问题规模或作为输入提供的数据。
我们可以使用算法分析来解决这些问题中的一些问题。当它有效时,算法分析使我们可以比较算法而不必实现它们。但是我们必须做出一些假设:
+
为了避免处理计算机硬件的细节,我们通常会识别构成算法的基本操作,如加法,乘法和数字比较,并计算每个算法所需的操作次数。
+
为了避免处理输入数据的细节,最好的选择是分析我们预期输入的平均性能。如果不可能,一个常见的选择是分析最坏的情况。
+
最后,我们必须处理一个可能性,一种算法最适合小问题,另一个算法适用于较大的问题。在这种情况下,我们通常专注于较大的问题,因为小问题的差异可能并不重要,但对于大问题,差异可能是巨大的。
这种分析适用于简单的算法分类。例如,如果我们知道算法
`A`
的运行时间通常与输入规模成正比,即
`n`
,并且算法
`B`
通常与
`n ** 2`
成比例,我们预计
`A`
比
`B`
更快,至少对于
`n`
的较大值。
大多数简单的算法只能分为几类。
+
常数时间:如果运行时间不依赖于输入的大小,算法是“常数时间”。例如,如果您有一个
`n`
个元素的数组,并且使用下标运算符(
`[]`
)来访问其中一个元素,则此操作将执行相同数量的操作,而不管数组有多大。
+
线性:如果运行时间与输入的大小成正比,则算法为“线性”的。例如,如果您计算数组的和,则必须访问
`n`
个元素并执行
`n - 1`
个添加。操作的总数(元素访问和加法)为
`2 * n -1`
,与
`n`
成正比。
+
平方:如果运行时间与
`n ** 2`
成正比,算法是“平方”的。例如,假设您要检查列表中的任何元素是否多次出现。一个简单的算法是将每个元素与其他元素进行比较。如果有
`n`
个元素,并且每个元素与
`n - 1`
个其他元素进行比较,则比较的总数是
`n ** 2 - n`
,随着
`n`
增长它与
`n ** 2`
成正比。
## 2.1 选择排序
例如,这是一个简单算法的实现,叫做“选择排序”(请见
<http://thinkdast.com/selectsort>
):
```
java
public
class
SelectionSort
{
/**
* Swaps the elements at indexes i and j.
*/
public
static
void
swapElements
(
int
[]
array
,
int
i
,
int
j
)
{
int
temp
=
array
[
i
];
array
[
i
]
=
array
[
j
];
array
[
j
]
=
temp
;
}
/**
* Finds the index of the lowest value
* starting from the index at start (inclusive)
* and going to the end of the array.
*/
public
static
int
indexLowest
(
int
[]
array
,
int
start
)
{
int
lowIndex
=
start
;
for
(
int
i
=
start
;
i
<
array
.
length
;
i
++)
{
if
(
array
[
i
]
<
array
[
lowIndex
])
{
lowIndex
=
i
;
}
}
return
lowIndex
;
}
/**
* Sorts the elements (in place) using selection sort.
*/
public
static
void
selectionSort
(
int
[]
array
)
{
for
(
int
i
=
0
;
i
<
array
.
length
;
i
++)
{
int
j
=
indexLowest
(
array
,
i
);
swapElements
(
array
,
i
,
j
);
}
}
}
```
第一个方法
`swapElements`
交换数组的两个元素。元素的是常数时间的操作,因为如果我们知道元素的大小和第一个元素的位置,我们可以使用一个乘法和一个加法来计算任何其他元素的位置,这都是常数时间的操作。由于
`swapElements`
中的一切都是恒定的时间,整个方法是恒定的时间。
第二个方法
`indexLowest`
从给定的索引
`start`
开始,找到数组中最小元素的索引。每次遍历循环的时候,它访问数组的两个元素并执行一次比较。由于这些都是常数时间的操作,因此我们计算什么并不重要。为了保持简单,我们来计算一下比较的数量。
+
如果
`start`
为
`0`
,则
`indexLowest`
遍历整个数组,并且比较的总数是数组的长度,我称之为
`n`
。
+
如果
`start`
为
`1`
,则比较数为
`n - 1`
。
+
一般情况下,比较的次数是
`n - start`
,因此
`indexLowest`
是线性的。
第三个方法
`selectionSort`
对数组进行排序。它从
`0`
循环到
`n - 1`
,所以循环执行了
`n`
次。每次调用
`indexLowest`
然后执行一个常数时间的操作
`swapElements`
。
第一次
`indexLowest`
被调用的时候,它进行
`n`
次比较。第二次,它进行
`n - 1`
比较,依此类推。比较的总数是
```
n + n−1 + n−2 + ... + 1 + 0
```
这个数列的和是
`n(n+1)/2`
,它(近似)与
`n ** 2`
成正比;这意味着
`selectionSort`
是平方的。
为了得到同样的结果,我们可以将
`indexLowest`
看作一个嵌套循环。每次调用
`indexLowest`
时,操作次数与
`n`
成正比。我们调用它
`n`
次,所以操作的总数与
`n ** 2`
成正比。
## 2.2 大 O 表示法
所有常数时间算法属于称为
`O(1)`
的集合。所以,说一个算法是常数时间的另一个方法就是,说它是
`O(1)`
的。与之类似,所有线性算法属于
`O(n)`
,所有二次算法都属于
`O(n ** 2)`
。这种分类算法的方式被称为“大 O 表示法”。
注意:我提供了一个大 O 符号的非专业定义。更多的数学处理请参见
<http://thinkdast.com/bigo>
。
这个符号提供了一个方便的方式,来编写通用的规则,关于算法在我们构造它们时的行为。例如,如果你执行线性时间算法,之后是常量算法,则总运行时间是线性的。
`∈`
表示“是...的成员”:
```
f ∈ O(n) && g ∈ O(1) => f + g ∈ O(n)
```
如果执行两个线性运算,则总数仍然是线性的:
```
f ∈ O(n) && g ∈ O(n) => f + g ∈ O(n)
```
事实上,如果你执行任何次数的线性运算,
`k`
,总数就是线性的,只要
`k`
是不依赖于
`n`
的常数。
```
f ∈ O(n) && k 是常数 => kf ∈ O(n)
```
但是,如果执行
`n`
次线性运算,则结果为平方:
```
f ∈ O(n) => nf ∈ O(n ** 2)
```
一般来说,我们只关心
`n`
的最大指数。所以如果操作总数为
`2 * n + 1`
,则属于
`O(n)`
。主要常数
`2`
和附加项
`1`
对于这种分析并不重要。与之类似,
`n ** 2 + 100 * n + 1000`
是
`O(n ** 2)`
的。不要被大的数值分心!
“增长级别”是同一概念的另一个名称。增长级别是一组算法,其运行时间在同一个大 O 分类中;例如,所有线性算法都属于相同的增长级别,因为它们的运行时间为
`O(n)`
。
在这种情况下,“级别”是一个团体,像圆桌骑士的阶级,这是一群骑士,而不是一种排队方式。因此,您可以将线性算法的阶级设想为一组勇敢,仗义,特别有效的算法。
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录