“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情”
第1章 绪论
1.1 计算机与算法
排序(sorting)
起泡排序 (bubblesort 1A):
经过k趟扫描交换之后,最大的前 k 个元素必然就位;经过k趟扫描交换之后,待求解问题的有效规模将缩减至n - k。
0001 void bubblesort1A ( int A[], int n ) { //起泡排序算法(版本1A):0 <= n
0002 bool sorted = false; //整体排序标志,首先假定尚未排序
0003 while ( !sorted ) { //在尚未确认已全局排序之前,逐趟进行扫描交换
0004 sorted = true; //假定已经排序
0005 for ( int i = 1; i < n; i++ ) { //自左向右逐对检查当前范围A[0, n)内的各相邻元素
0006 if ( A[i - 1] > A[i] ) { //一旦A[i - 1]与A[i]逆序,则
0007 swap ( A[i - 1], A[i] ); //交换之,并
0008 sorted = false; //因整体排序不能保证,需要清除排序标志
0009 }
0010 }
0011 n--; //至此末元素必然就位,故可以缩短待排序序列的有效长度
0012 }
0013 } //借助布尔型标志位sorted,可及时提前退出,而不致总是蛮力地做n - 1趟扫描交换
1.2 复杂度度量
时间复杂度 T(n)
渐近复杂度
-
以大O记号形式表示时间复杂度,实质上是对算法执行时间的一种保守估计
-
与大O记号恰好相反,大Ω(omega)记号是对算法执行效率的乐观估计
-
大Θ(theta)记号是对算法复杂度的准确估计
空间复杂度
1.3 复杂度分析
常数O(1)
对数O(logn)
线性O(n)
多项式O(polynomial(n))
指数
in-place algorithm:就地算法:就地算法在不使用任何额外内存的情况下转换输入,常仅需O(1)辅助空间
out-place algorithm:非就地算法
1.4 递归
递归是函数和过程调用的一种特殊形式,即允许函数和过程进行自我调用。
递归的价值在于,许多应用问题都可简洁而准确地描述为递归形式。
递归也是一种基本而典型的算法设计模式。
1.4.1 线性递归
算法可能朝着更深一层进行自我调用,且每一递归实例对自身调用至多一次。于是,每一层次上至多只有一个实例,且它们构成一个线性的次序关系。此类递归模式因而称作线性递归(linear recursion),也是递归最基本形式。
线性递归的模式,往往对应于所谓减而治之的算法策略:递归每深入一层,待求解问题的规模都缩减一个常数,直至最终蜕化为平凡的小问题。
递推方程法。
1.4.2 二分递归
“凡治众如治寡,分数是也”。分而治之策略:将问题分解为若干规模更小的子问题,再通过递归机制分别求解。这种分解持续进行,直到子问题规模缩减至平凡问题。
Fibonacci数:经典二分递归
0001 __int64 fib ( int n ) { //计算Fibonacci数列的第n项(二分递归版):O(2^n)
0002 return ( 2 > n ) ?
0003 ( __int64 ) n //若到达递归基,直接取值
0004 : fib ( n - 1 ) + fib ( n - 2 ); //否则,递归计算前两项,其和即为正解
0005 }
优化策略:
为消除递归算法中重复的递归实例,一种自然而然的思路和技巧,可以概括为:
借助一定量的辅助空间,在各子问题求解之后,及时记录下其对应的解答:
- 制表(tabulation)或记忆策略(memorization):从原问题出发自顶向下,每当遇到一个子问题,都首先查验它是否已经计算过,以期通过直接调阅记录获得解答,从而避免重新计算。
- 动态规划(dynamic programming) :从递归基出发,自底而上递推得出各子问题的解,直至最终原问题解。
Fibonacci数:线性递归 —— 制表策略 T(n) = O(n),S(n) = O(n)
0001 __int64 fib ( int n, __int64& prev ) { //计算Fibonacci数列第n项(线性递归版):入口形式fib(n, prev)
0002 if ( 0 == n ) //若到达递归基,则
0003 { prev = 1; return 0; } //直接取值:fib(-1) = 1, fib(0) = 0
0004 else { //否则
0005 __int64 prevPrev; prev = fib ( n - 1, prevPrev ); //递归计算前两项
0006 return prevPrev + prev; //其和即为正解
0007 }
0008 } //用辅助变量记录前一项,返回数列的当前项,O(n)
Fibonacci数:迭代 —— 动态规划策略 T(n) = O(n),S(n) = O(1)
0001 __int64 fibI ( int n ) { //计算Fibonacci数列的第n项(迭代版):O(n)
0002 __int64 f = 1, g = 0; //初始化:fib(-1)、fib(0)
0003 while ( 0 < n-- ) { g += f; f = g - f; } //依据原始定义,通过n次加法和减法计算fib(n),精髓!
0004 return g; //返回
0005 }
1.5 抽象数据类型
现代数据结构普遍遵从“信息隐藏”的理念,通过统一接口和内部封装,分层次从整体上加以设计、实现与使用。
所谓封装,就是将数据项与相关的操作结合为一个整体,并将其从外部的可见性划分为若干级别,从而将数据结构的外部特性与其内部实现相分离,提供一致且标准的对外接口,隐藏内部的实现细节。
抽象数据类型(abstract data type, ADT):由数据集合及其对应的操作可超脱于具体的程序设计语言、具体的实现方式所构成。
抽象数据类型的理论催生了现代面向对象的程序设计语言,而支持封装也是此类语言的基本特征。