算法 - 算法分析.md 7.0 KB
Newer Older
C
CyC2018 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
<!-- GFM-TOC -->
* [数学模型](#数学模型)
    * [1. 近似](#1-近似)
    * [2. 增长数量级](#2-增长数量级)
    * [3. 内循环](#3-内循环)
    * [4. 成本模型](#4-成本模型)
* [注意事项](#注意事项)
    * [1. 大常数](#1-大常数)
    * [2. 缓存](#2-缓存)
    * [3. 对最坏情况下的性能的保证](#3-对最坏情况下的性能的保证)
    * [4. 随机化算法](#4-随机化算法)
    * [5. 均摊分析](#5-均摊分析)
* [ThreeSum](#threesum)
    * [1. ThreeSumSlow](#1-threesumslow)
    * [2. ThreeSumBinarySearch](#2-threesumbinarysearch)
    * [3. ThreeSumTwoPointer](#3-threesumtwopointer)
* [倍率实验](#倍率实验)
<!-- GFM-TOC -->


# 数学模型

##  1. 近似

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

##  2. 增长数量级

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

##  3. 内循环

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

##  4. 成本模型

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

# 注意事项

##  1. 大常数

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

##  2. 缓存

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

##  3. 对最坏情况下的性能的保证

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

##  4. 随机化算法

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

##  5. 均摊分析

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

# ThreeSum

ThreeSum 用于统计一个数组中和为 0 的三元组数量。

```java
public interface ThreeSum {
    int count(int[] nums);
}
```

##  1. ThreeSumSlow

该算法的内循环为 `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>)。

```java
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;
    }
}
```

##  2. ThreeSumBinarySearch

将数组进行排序,对两个元素求和,并用二分查找方法查找是否存在该和的相反数,如果存在,就说明存在和为 0 的三元组。

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

该方法可以将 ThreeSum 算法增长数量级降低为 O(N<sup>2</sup>logN)。

```java
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;
    }
}
```

```java
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;
    }
}
```

##  3. ThreeSumTwoPointer

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

C
CyC2018 已提交
150 151
同样不适用与数组存在重复元素的情况。

C
CyC2018 已提交
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
```java
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];
            while (l < h) {
                int sum = nums[l] + nums[h];
                if (sum == target) {
                    cnt++;
                    l++;
                    h--;
                } else if (sum < target) {
                    l++;
                } else {
                    h--;
                }
            }
        }
        return cnt;
    }
}
```

# 倍率实验

如果 T(N) \~ aN<sup>b</sup>logN,那么 T(2N)/T(N) \~ 2<sup>b</sup>

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

| 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 |

可以看到,T(2N)/T(N) \~ 2<sup>3</sup>,因此可以确定 T(N) \~ aN<sup>3</sup>logN。

```java
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;
        }
    }
}
```

```java
public class StopWatch {

    private static long start;


    public static void start() {
        start = System.currentTimeMillis();
    }


    public static double elapsedTime() {
        long now = System.currentTimeMillis();
        return (now - start) / 1000.0;
    }
}
```




C
CyC2018 已提交
241
<img width="580px" src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/other/公众号海报1.png"></img>