程序员必须掌握哪些算法和数据结构?

作为一名程序员,大家有没有想过:编码最本质的知识是什么?或许是算法和数据结构,至少很多人这么认为。

本场 Chat 从以下几个方面讨论算法的性能:
  1. 算法研究的科学方法;
  2. 编写衡量算法的时间性能类 StopWatch;
  3. ThreeSum 的例子阐述算法的方方面面;
  4. 衡量时间复杂度的一种简单度量:波浪线表示;
  5. 一些典型的 Order of Growth, 比如 log2n, n, nlog2n, n2 , n3;
  6. 分析 Java 中各种类型的内存消耗,包括原生类型,对象类型,字符串和数组。


1. 科学方法(Scientific method)

以下 5 个步骤总结了此方法,依次为如下,我们设计的实验必须是可以重现的,我们形成的假设必须是具有真伪的。

程序员必须掌握哪些算法和数据结构?_第1张图片

2. 时间度量

测试程序运行的精确时间有时是困难的,但是我们有许多辅助工具。在这里,我们简化程序运行时间的模型,考虑各种输入情况,并测试每种情况下的运行时间,编写的这个程序名称为:Stopwatch.java,如下所示:

 1 public class Stopwatch {  2 3    private final long start; 4 5    public Stopwatch() { 6        start = System.currentTimeMillis(); 7    } 8 9    public double elapsedTime() {10        long now = System.currentTimeMillis();11        return (now - start) / 1000.0;12    }1314    public static void main(String[] args) {15        int n = Integer.parseInt(args[0]);1617        // sum of square roots of integers from 1 to n using Math.sqrt(x).18        Stopwatch timer1 = new Stopwatch();19        double sum1 = 0.0;20        for (int i = 1; i <= n; i++) {21            sum1 += Math.sqrt(i);22        }23        double time1 = timer1.elapsedTime();24        StdOut.printf("%e (%.2f seconds)\n", sum1, time1);2526        // sum of square roots of integers from 1 to n using Math.pow(x, 0.5).27        Stopwatch timer2 = new Stopwatch();28        double sum2 = 0.0;29        for (int i = 1; i <= n; i++) {30            sum2 += Math.pow(i, 0.5);31        }32        double time2 = timer2.elapsedTime();33        StdOut.printf("%e (%.2f seconds)\n", sum2, time2);34    }35}

对于大多数程序,首先我们能想到的量化观察是它们有问题的大小(problem size)区别,这个表征了计算复杂度或计算难度。

一般地,问题大小既可以指通过输入数据的大小,也可以指通过命令行参数输入值。

直觉上,运行时间应该会随着问题大小而增加,但是增加的程度怎么度量,这是我们编程运行程序时常遇到的问题。

3. ThreeSum 例子

为了阐述方法,我们先引入一个具体的编程问题:ThreeSum,它是在给定的含有 n 个元素的数组中找出三元组之和等于 0 的个数。

这个问题最简单的一个解法:枚举。代码如下:

 1 public class ThreeSum { 2 3    // print distinct triples (i, j, k) such that a[i] + a[j] + a[k] = 0 4    public static void printAll(int[] a) { 5        int n = a.length; 6        for (int i = 0; i < n; i++) { 7            for (int j = i+1; j < n; j++) { 8                for (int k = j+1; k < n; k++) { 9                    if (a[i] + a[j] + a[k] == 0) {10                        StdOut.println(a[i] + " " + a[j] + " " + a[k]);11                    }12                }13            }14        }15    }1617    // return number of distinct triples (i, j, k) such that a[i] + a[j] + a[k] = 018    public static int count(int[] a) {19        int n = a.length;20        int count = 0;21        for (int i = 0; i < n; i++) {22            for (int j = i+1; j < n; j++) {23                for (int k = j+1; k < n; k++) {24                    if (a[i] + a[j] + a[k] == 0) {25                        count++;26                    }27                }28            }29        }30        return count;31    }3233    public static void main(String[] args)  {34        int[] a = StdIn.readAllInts();35        Stopwatch timer = new Stopwatch();36        int count = count(a);37        StdOut.println("elapsed time = " + timer.elapsedTime());38        StdOut.println(count);39    }40}

我们在这里主要关心的是输入数据大小与运行时长的关系。我们循着如下的思路分析两者间的关系。

3.1 加倍假设

加倍假设(Doubling hypothesis)对于大量的程序而言,我们能很快地形成如下假设:假如输入数据的个数加倍,运行时间怎么变化。

3.2 经验分析

经验分析(Empirical analysis)一种简单的实现加倍假设的方法是使输入数据的个数加倍,然后观察对运行时长的影响。 如下所示为一个简单的通过加倍输入个数,测试运行时长的程序:DoublingTest.Java。
 1public class DoublingTest { 2 3    public static double timeTrial(int n) { 4        int[] a = new int[n]; 5        for (int i = 0; i < n; i++) { 6            a[i] = StdRandom.uniform(2000000) - 1000000; 7        } 8        Stopwatch s = new Stopwatch(); 9        ThreeSum.count(a);10        return s.elapsedTime();11    }121314    public static void main(String[] args) {15        StdOut.printf("%7s %7s %4s\n", "size", "time", "ratio");16        double previous = timeTrial(256);17        for (int n = 512; true; n += n) {18            double current = timeTrial(n);19            StdOut.printf("%7d %7.2f %4.2f\n", n, current, current / previous);20            previous = current;21        }22    }23}

再回到 ThreeSum.java 程序中,我们生成一系列随机数,填充到输入数组中,每一个时步都加倍输入元素个数,然后观察到每一次程序所运行的时间都会大致增加 8 倍,这个就可以让我们下结论说输入数据加倍后运行时间增加了 8 倍。

如下图中左侧视图所示,当输入大小为 4K 时,运行时长为 64T,当输入带下为 8K 时,运行时长变为原来的 8 倍:512T。

程序员必须掌握哪些算法和数据结构?_第2张图片

Log–logplot:绘制 log 图也是衡量运行时间的一种方法,因为是log级别的,所以与上面说的经验公式没有本质区别,如图右所示,取完对数后,输入数据大小与运行时间的对数变为线性关系。

3.3 数学分析

总的运行时长受到两个主要指标影响:

  • 执行每一个语句的成本
  • 执行每一个语句的次数
其中,第一个是系统属性,后面一个是算法的属性。假如我们知道了程序中所有指令的这两个属性值,那么两者相乘求和后便是整个程序的运行时间。 主要的挑战是确定第二个指标,即语句的执行次数。一些语句是很容易分析出执行次数,比如将 count 设置初始值为 0,在 ThreeSum.count() 中仅仅执行一次。

但是,有些需要更高层次的推理演算才能得到语句的执行频次,比如 if 语句被执行的次数恰好为:n(n-1)(n-2)/6。

限于篇幅,想看完整原文的同学可以扫码查看

程序员必须掌握哪些算法和数据结构?_第3张图片


点击阅读原文,免费订阅本场 Chat 全文。

你可能感兴趣的:(程序员必须掌握哪些算法和数据结构?)