提交 6132c1e1 编写于 作者: W wizardforcel

ch2.

上级 12b0236c
# 第二章 算法分析
我们在前面的章节中看到,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.
先完成此消息的编辑!
想要评论请 注册