数据结构 | 第1章 绪论

115 阅读5分钟

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情

第1章 绪论

1.1 计算机与算法

排序(sorting)

起泡排序 (bubblesort 1A):

image-20220618203014082.png

经过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记号形式表示时间复杂度,实质上是对算法执行时间的一种保守估计

    image-20220618214350175.png

  • 与大O记号恰好相反,大Ω(omega)记号是对算法执行效率的乐观估计

  • 大Θ(theta)记号是对算法复杂度的准确估计

image-20220618213717144.png

空间复杂度

1.3 复杂度分析

常数O(1)

对数O(logn)

线性O(n)

多项式O(polynomial(n))

指数O(2n)O(2^n)

image-20220618214921498.png

in-place algorithm:就地算法:就地算法在不使用任何额外内存的情况下转换输入,常仅需O(1)辅助空间

out-place algorithm:非就地算法

image-20220618220714617.png

1.4 递归

递归是函数和过程调用的一种特殊形式,即允许函数和过程进行自我调用。

递归的价值在于,许多应用问题都可简洁而准确地描述为递归形式。

递归也是一种基本而典型的算法设计模式。

1.4.1 线性递归

算法可能朝着更深一层进行自我调用,且每一递归实例对自身调用至多一次。于是,每一层次上至多只有一个实例,且它们构成一个线性的次序关系。此类递归模式因而称作线性递归(linear recursion),也是递归最基本形式。

线性递归的模式,往往对应于所谓减而治之的算法策略:递归每深入一层,待求解问题的规模都缩减一个常数,直至最终蜕化为平凡的小问题。

递推方程法。

1.4.2 二分递归

“凡治众如治寡,分数是也”。分而治之策略:将问题分解为若干规模更小的子问题,再通过递归机制分别求解。这种分解持续进行,直到子问题规模缩减至平凡问题。

Fibonacci数:经典二分递归T(n)=O(2n) T(n) = O(2^n)

 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):由数据集合及其对应的操作可超脱于具体的程序设计语言、具体的实现方式所构成。

抽象数据类型的理论催生了现代面向对象的程序设计语言,而支持封装也是此类语言的基本特征。