算法 - 算法分析.md 7.9 KB
Newer Older
C
CyC2018 已提交
1
# 数学模型
C
CyC2018 已提交
2

C
CyC2018 已提交
3
##  1. 近似
C
CyC2018 已提交
4

C
CyC2018 已提交
5
N<sup>3</sup>/6-N<sup>2</sup>/2+N/3 ~ N<sup>3</sup>/6。使用 ~f(N) 来表示所有随着 N 的增大除以 f(N) 的结果趋近于 1 的函数。
C
CyC2018 已提交
6

C
CyC2018 已提交
7
##  2. 增长数量级
C
CyC2018 已提交
8

C
CyC2018 已提交
9
N<sup>3</sup>/6-N<sup>2</sup>/2+N/3 的增长数量级为 O(N<sup>3</sup>)。增长数量级将算法与它的实现隔离开来,一个算法的增长数量级为 O(N<sup>3</sup>) 与它是否用 Java 实现,是否运行于特定计算机上无关。
C
CyC2018 已提交
10

C
CyC2018 已提交
11
##  3. 内循环
C
CyC2018 已提交
12 13 14

执行最频繁的指令决定了程序执行的总时间,把这些指令称为程序的内循环。

C
CyC2018 已提交
15
##  4. 成本模型
C
CyC2018 已提交
16 17 18

使用成本模型来评估算法,例如数组的访问次数就是一种成本模型。

C
CyC2018 已提交
19
# 注意事项
C
CyC2018 已提交
20

C
CyC2018 已提交
21
##  1. 大常数
C
CyC2018 已提交
22 23 24

在求近似时,如果低级项的常数系数很大,那么近似的结果就是错误的。

C
CyC2018 已提交
25
##  2. 缓存
C
CyC2018 已提交
26 27 28

计算机系统会使用缓存技术来组织内存,访问数组相邻的元素会比访问不相邻的元素快很多。

C
CyC2018 已提交
29
##  3. 对最坏情况下的性能的保证
C
CyC2018 已提交
30 31 32

在核反应堆、心脏起搏器或者刹车控制器中的软件,最坏情况下的性能是十分重要的。

C
CyC2018 已提交
33
##  4. 随机化算法
C
CyC2018 已提交
34 35 36

通过打乱输入,去除算法对输入的依赖。

C
CyC2018 已提交
37
##  5. 均摊分析
C
CyC2018 已提交
38

C
CyC2018 已提交
39
将所有操作的总成本除于操作总数来将成本均摊。例如对一个空栈进行 N 次连续的 push() 调用需要访问数组的次数为 N+4+8+16+...+2N=5N-4(N 是向数组写入元素的操作次数,其余的都是调整数组大小时进行复制需要的访问数组次数),均摊后访问数组的平均次数为常数。
C
CyC2018 已提交
40

C
CyC2018 已提交
41
# ThreeSum
C
CyC2018 已提交
42

C
CyC2018 已提交
43
ThreeSum 用于统计一个数组中和为 0 的三元组数量。
C
CyC2018 已提交
44 45

```java
C
CyC2018 已提交
46 47
public interface ThreeSum {
    int count(int[] nums);
C
CyC2018 已提交
48 49 50
}
```

C
CyC2018 已提交
51
##  1. ThreeSumSlow
C
CyC2018 已提交
52

C
CyC2018 已提交
53
该算法的内循环为 `if (nums[i] + nums[j] + nums[k] == 0)` 语句,总共执行的次数为 N(N-1)(N-2) = N<sup>3</sup>/6-N<sup>2</sup>/2+N/3,因此它的近似执行次数为 ~N<sup>3</sup>/6,增长数量级为 O(N<sup>3</sup>)。
C
CyC2018 已提交
54 55

```java
C
CyC2018 已提交
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
public class ThreeSumSlow implements ThreeSum {
    @Override
    public int count(int[] nums) {
        int N = nums.length;
        int cnt = 0;
        for (int i = 0; i < N; i++) {
            for (int j = i + 1; j < N; j++) {
                for (int k = j + 1; k < N; k++) {
                    if (nums[i] + nums[j] + nums[k] == 0) {
                        cnt++;
                    }
                }
            }
        }
        return cnt;
    }
C
CyC2018 已提交
72 73 74
}
```

C
CyC2018 已提交
75
##  2. ThreeSumBinarySearch
C
CyC2018 已提交
76

C
CyC2018 已提交
77
通过将数组先排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在三元组的和为 0。
C
CyC2018 已提交
78 79 80

应该注意的是,只有数组不含有相同元素才能使用这种解法,否则二分查找的结果会出错。

C
CyC2018 已提交
81
该方法可以将 ThreeSum 算法增长数量级降低为 O(N<sup>2</sup>logN)。
C
CyC2018 已提交
82 83

```java
C
CyC2018 已提交
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
public class ThreeSumBinarySearch implements ThreeSum {

    @Override
    public int count(int[] nums) {
        Arrays.sort(nums);
        int N = nums.length;
        int cnt = 0;
        for (int i = 0; i < N; i++) {
            for (int j = i + 1; j < N; j++) {
                int target = -nums[i] - nums[j];
                int index = BinarySearch.search(nums, target);
                // 应该注意这里的下标必须大于 j,否则会重复统计。
                if (index > j) {
                    cnt++;
                }
            }
        }
        return cnt;
    }
C
CyC2018 已提交
103 104 105 106
}
```

```java
C
CyC2018 已提交
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
public class BinarySearch {

    public static int search(int[] nums, int target) {
        int l = 0, h = nums.length - 1;
        while (l <= h) {
            int m = l + (h - l) / 2;
            if (target == nums[m]) {
                return m;
            } else if (target > nums[m]) {
                l = m + 1;
            } else {
                h = m - 1;
            }
        }
        return -1;
    }
C
CyC2018 已提交
123 124 125
}
```

C
CyC2018 已提交
126
##  3. ThreeSumTwoPointer
C
CyC2018 已提交
127

C
CyC2018 已提交
128
更有效的方法是先将数组排序,然后使用双指针进行查找,时间复杂度为 O(N<sup>2</sup>)。
C
CyC2018 已提交
129 130

```java
C
CyC2018 已提交
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
public class ThreeSumTwoPointer implements ThreeSum {

    @Override
    public int count(int[] nums) {
        int N = nums.length;
        int cnt = 0;
        Arrays.sort(nums);
        for (int i = 0; i < N - 2; i++) {
            int l = i + 1, h = N - 1, target = -nums[i];
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            while (l < h) {
                int sum = nums[l] + nums[h];
                if (sum == target) {
                    cnt++;
                    while (l < h && nums[l] == nums[l + 1]) l++;
                    while (l < h && nums[h] == nums[h - 1]) h--;
                    l++;
                    h--;
                } else if (sum < target) {
                    l++;
                } else {
                    h--;
                }
            }
        }
        return cnt;
    }
C
CyC2018 已提交
158 159 160
}
```

C
CyC2018 已提交
161
# 倍率实验
C
CyC2018 已提交
162

C
CyC2018 已提交
163
如果 T(N) ~ aN<sup>b</sup>logN,那么 T(2N)/T(N) ~ 2<sup>b</sup>
C
CyC2018 已提交
164

C
CyC2018 已提交
165
例如对于暴力的 ThreeSum 算法,近似时间为 ~N<sup>3</sup>/6。进行如下实验:多次运行该算法,每次取的 N 值为前一次的两倍,统计每次执行的时间,并统计本次运行时间与前一次运行时间的比值,得到如下结果:
C
CyC2018 已提交
166

C
CyC2018 已提交
167 168 169 170 171 172 173 174
| N | Time(ms) | Ratio |
| :---: | :---: | :---: |
| 500 | 48 | / |
| 1000 | 320 | 6.7 |
| 2000 | 555 | 1.7 |
| 4000 | 4105 | 7.4 |
| 8000 | 33575 | 8.2 |
| 16000 | 268909 | 8.0 |
C
CyC2018 已提交
175

C
CyC2018 已提交
176
可以看到,T(2N)/T(N) ~ 2<sup>3</sup>,因此可以确定 T(N) ~ aN<sup>3</sup>logN。
C
CyC2018 已提交
177 178

```java
C
CyC2018 已提交
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
public class RatioTest {

    public static void main(String[] args) {
        int N = 500;
        int loopTimes = 7;
        double preTime = -1;
        while (loopTimes-- > 0) {
            int[] nums = new int[N];
            StopWatch.start();
            ThreeSum threeSum = new ThreeSumSlow();
            int cnt = threeSum.count(nums);
            System.out.println(cnt);
            double elapsedTime = StopWatch.elapsedTime();
            double ratio = preTime == -1 ? 0 : elapsedTime / preTime;
            System.out.println(N + "  " + elapsedTime + "  " + ratio);
            preTime = elapsedTime;
            N *= 2;
        }
    }
C
CyC2018 已提交
198 199 200 201
}
```

```java
C
CyC2018 已提交
202
public class StopWatch {
C
CyC2018 已提交
203

C
CyC2018 已提交
204
    private static long start;
C
CyC2018 已提交
205 206


C
CyC2018 已提交
207 208 209
    public static void start() {
        start = System.currentTimeMillis();
    }
C
CyC2018 已提交
210 211


C
CyC2018 已提交
212 213 214 215
    public static double elapsedTime() {
        long now = System.currentTimeMillis();
        return (now - start) / 1000.0;
    }
C
CyC2018 已提交
216 217
}
```