第一章:基本概念

什么是数据结构 ?

其实官方没有统一定义!!!

“数据结构是数据对象,以及存在于该对象的实例和组成实例的数据元素之间的各种联系。这种联系可以通过定义相关的函数给出。” - Sartaj Sahni 《数据结构、算法与应用》

“数据结构是ADT(抽象数据类型 Abstract Data Type)的物理实现。” - Clifford A.Shaffer 《数据结构与算法分析》

“数据结构(data structure)是计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来最优效率的算法。” - 中文维基百科

灵魂拷问之如何在书架上摆放图书?

我刚听到这个问题时,第一反应是我知道怎么摆放,就先分类,然后按名称顺序排呗!其实不是的,首先这个问题就不严谨,它没有明确说书架是什么样的,家里的书架能和图书馆的一样嘛!规模差很多的,自然摆放方式就会不同(下面我们考虑如何在图书馆里摆放书籍)。

其实有两个主要的操作(接下来方法的比较将从这两点出发):

1、新书插入
2、找到某本指定书籍

方法 1:随便放

  • 操作 1:新书插入
    哪里有空放哪里,一步到位!
  • 操作 2:找到指定书籍
    …累死找不到

方法 2:按照书名的拼音字母顺序排放

  • 操作 1:新书插入
    新进一本《啊Q正传》 …,完蛋,后面的全部移动一遍
  • 操作 2:找到指定书籍
    二分查找

方法 3:先区域类别划分,然后按照书名顺序排放

  • 操作 1:新书插入
    先定类别,二分查找确定位置,移出空位
  • 操作 2:找到指定书籍
    先定类别,再二分查找

总结:其实书架可以当作是计算机中的存储,而插入规则可以看作是数据的组织方式,此时你应该对数据结构有一定了解了。

什么是算法 ?

算法是指对特定问题求解步骤的一种描述。
算法只是对问题求解方法的一种描述,它不依赖于任何一种语言,既可以用自然语言、程序设计语言(C、C++、Java、Python 等)描述,也可以用流程图、框图来表示。

在我们的生活中,算法无处不在。我们每天早上起来,刷牙、洗脸、吃早餐,都是算着时间,以免上班或上课迟到等。其实这些都是算法。

算法的特征

  1. 有穷性:算法是由若干条指令组成的有穷序列,总是在执行若干次后结束,不可能永不停止。
  2. 确定性:每条语句有确定的含义,无歧义。
  3. 可行性:算法在当前环境条件下可以通过有限次运算实现。
  4. 输入输出:有零个或多个输入,一个或多个输出。

“好”算法具有以下特征:

  1. 正确性:指算法能够满足具体问题的需求,程序运行正常,无语法错误,能够通过典型的软件测试,达到预期的需求。
  2. 易读性:算法遵循标识符命名规则,简洁易懂,注释语句恰当适量,方便自己和他人阅读,便于后期调试和修改。
  3. 健壮性:对非法数据及操作有较好的反应和处理。
  4. 高效性:指算法运行效率高,即算法运行所消耗的时间短。算法时间复杂度就是算法运行需要的时间。
  5. 低存储性:指算法所需要的存储空间低。算法占用的空间大小称为空间复杂度

除了 1-3 中的基本标准外,我们对好的算法的评判标准就是**高效率、低存储

算法复杂性

时间复杂度

算法运行需要的时间,一般将算法的执行次数作为时间复杂度的度量标准。

举个例子:

int sum = 0;                        // 运行1次
int total = 0;                      // 运行1次
for (int i = 1; i <= n; i++) {      // 运行n+1次
    sum += i;                       // 运行n次
    for (int j = 1; j <= n; j++) {  // 运行n*(n+1)次
        total += i*j;               // 运行n*n次
    }
}

把所有语句的运行次数加起来: 1 + 1 + n + 1 + n + n ∗ ( n + 1 ) + n ∗ n 1+1+n+1+n+n*(n+1)+n*n 1+1+n+1+n+n(n+1)+nn,可以用一个函数T(n)表达:
T ( n ) = 2 n 2 + 3 n + 3 T(n)=2n^2+3n+3 T(n)=2n2+3n+3

当 n 足够大时,例如 n = 1 0 5 n=10^5 n=105 时, T ( n ) = 2 × 1 0 10 + 3 × 1 0 5 + 3 T(n)=2\times10^{10}+3\times10^5+3 T(n)=2×1010+3×105+3,我们可以看到算法运行时间主要取决于第一项,后面的甚至可以忽略不计。

注意:不是每个算法都能直接计算运行次数。有些算法,如排序、查找、插入等算法,可以分为最好、最坏和平均情况分别求算法渐近复杂度,但我们考查一个算法通常考查最坏的情况,而不是考查最好的情况,最坏情况对衡量算法的好坏具有实际的意义

空间复杂度

算法占用的空间大小。一般将算法的辅助空间作为衡量空间复杂度的标准。

举个例子:

void swap(int x, int y) 
{
    int temp;
    temp = x;  // temp 为辅助空间
    x = y;
    y = temp;
}

趣味故事:一棋盘的麦子

有一个古老的传说,有一个国王的女儿不幸落水,水中有很多鳄鱼,国王情急之下下令:“谁能把公主救上来,就把女儿嫁给他。”很多人纷纷退让,一个勇敢的小伙子挺身而出,冒着生命危险把公主救了上来,国王一看是个穷小子,想要反悔,说:“除了女儿,你要什么都可以。”小伙子说:“好吧,我只要一棋盘的麦子。您在第 1 个格子里放一粒麦子,在第 2 个格子里放 2 粒,在第 3 个格子里放 4 粒,在第四个格子里放 8 粒,以此类推,每一个格子里的麦子粒数都是前一格的两倍。把这 64 个格子都放好了就行,我就要这么多。”国王听后哈哈大笑,觉得小伙子的要求很容易满足,满口答应。结果发现,把全国的麦子都拿来,也填不完这 64 格…国王无奈,只好把女儿嫁给了这个小伙子。(学好算法能娶到公主!!!

解析:

棋盘上的 64 个格子究竟要放多少粒麦子?

S = 1 + 2 1 + 2 2 + 2 3 + . . . + 2 63 S=1+2^1+2^2+2^3+...+2^{63} S=1+21+22+23+...+263

转换一下

S = 2 64 − 1 S=2^{64}-1 S=2641

据专辑统计,每一麦粒的平均重量约 41.9 毫克,那么这些麦粒的总重量是:

18446744073709551615 × 41.9 = 772918576688430212668.5 (毫克) ≈ 7729 (亿吨) 18446744073709551615\times41.9=772918576688430212668.5(毫克)\approx7729(亿吨) 18446744073709551615×41.9=772918576688430212668.5(毫克)7729(亿吨)

全世界人口按 60 亿计算,每人可以分到 128 吨!

我们称这样的函数为爆炸增量函数,想一想,如果算法时间复杂度是 O ( 2 n ) O(2^n) O(2n)会怎样?随着 n 的增长,这个算法会不会“爆掉”?

常见的算法时间复杂度分类:

  1. 常数阶:运行的次数是一个常数,如 5、20、100。常数阶算法时间复杂度通常用 O ( 1 ) O(1) O(1)表示。
  2. 多项式阶:很多算法时间复杂度是多项式,通常用 O ( n ) 、 O ( n 2 ) 、 O ( n 3 ) O(n)、O(n^2)、O(n^3) O(n)O(n2)O(n3)等表示。
  3. 指数阶:指数阶时间复杂度运行效率极差,程序员往往像躲“恶魔”一样避开它。常见的有 O ( 2 n ) 、 O ( n ! ) 、 O ( n n ) O(2^n)、O(n!)、O(n^n) O(2n)O(n!)O(nn)等。使用这样的算法要慎重,例如上面的趣味故事。
  4. 对数阶:时间复杂度运行效率较高,常见的有 O ( log ⁡ n ) 、 O ( n log ⁡ n ) O({\log}n)、O(n{\log}n) O(logn)O(nlogn)等。

常见时间复杂度函数曲线如图:

在这里插入图片描述

从图中可以看出,指数阶增量随着x的增加而急剧增加,而对数阶增加缓慢。它们之间的关系为:

O ( 1 ) < O ( log ⁡ n ) < O ( n ) < O ( n log ⁡ n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)<O(\log{n})<O(n)<O(n\log{n})<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

我们在设计算法时要注意算法复杂度增量的问题,尽量避免爆炸级增量。

趣味故事:兔子序列(斐波那契数列)

f ( n ) { 1 , n = 1 1 , n = 2 f ( n − 2 ) + f ( n − 1 ) , n > 2 f(n) \begin{cases} 1, &n=1 \\ 1, &n=2 \\ f(n-2)+f(n-1), &n>2 \end{cases} f(n) 1,1,f(n2)+f(n1),n=1n=2n>2

递归实现:

#include <QCoreApplication>
#include <QTime>

#include <iostream>

using namespace std;

long double fib(int n) {
    long double temp;
    if (n < 1) {
        return -1;
    } else if (n == 1 || n == 2) {
        return 1;
    } else {
        return fib(n - 1) + fib(n - 2);
    }
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    int n = 0;
    cout << "Please input n:" << endl;
    cin >> n;

    QTime timer;
    timer.start();

    int fibn = fib(n);

    cout << "fibn:" << fibn << endl;
    cout << "total time:" << timer.elapsed() << endl;

    return a.exec();
}

但实际你自己输入试试后就会发现,效率十分低,因为它会重复计算很多,看一下 n=50 时候的时间。

在这里插入图片描述

看到这个时间,应该吓一跳吧。如果 n 再大一点的话更吓人!那我们就不使用递归了,换一种算法吧。

循环实现

long double fib(int n) {
    if (n < 1) {
        return -1;
    } else if (n == 1 || n == 2) {
        return 1;
    }

    int s1 = 1;
    int s2 = 1;
    
    for (int i = 3; i <= n; i++) {
        s2 = s1 + s2; // 辗转相加法
        s1 = s2 - s1; // 记录前一项
    }
   return s2;
}

我们使用了若干个辅助变量,迭代辗转相加,每次记录前一项,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1),是不是比递归好太多了。是不是还有更好的算法呢?那肯定是有的,大家可以自行去研究一下。

数据结构与算法

瑞士著名的科学家 N.Wirth 教授曾提出:数据结构+算法=程序

数据结构与算法之间存在着密切的关系。算法是一种解决问题的方法或步骤,它们通过操作数据结构来实现。算法可以实现各种功能,如插入、排序、搜索、图遍历等。不同的算法对于不同的问题具有不同的时间复杂度和空间复杂度。因此,选择合适的数据结构和算法对程序的性能至关重要。

数据结构与算法的重要性:

  • 它们可以提高程序的执行效率。通过选择适当的数据结构和算法,我们可以降低时间和空间的消耗,减少程序的运行时间。这对于大规模数据处理和复杂应用程序来说尤为重要。

  • 它们有助于提高代码的可读性和可维护性。良好的数据结构和算法设计可以使代码更加清晰和易于理解。这可以帮助开发人员更好地理解代码逻辑,减少出错的可能性,并且方便代码的维护和拓展。

  • 数据结构与算法也是衡量编程能力的重要指标之一。熟练掌握数据结构与算法的知识和技巧,可以提高我们解决实际问题的能力,并且在面试和竞争中具备一定的优势。
    们可以提高程序的执行效率。通过选择适当的数据结构和算法,我们可以降低时间和空间的消耗,减少程序的运行时间。这对于大规模数据处理和复杂应用程序来说尤为重要。

  • 它们有助于提高代码的可读性和可维护性。良好的数据结构和算法设计可以使代码更加清晰和易于理解。这可以帮助开发人员更好地理解代码逻辑,减少出错的可能性,并且方便代码的维护和拓展。

  • 数据结构与算法也是衡量编程能力的重要指标之一。熟练掌握数据结构与算法的知识和技巧,可以提高我们解决实际问题的能力,并且在面试和竞争中具备一定的优势。

猜你喜欢

转载自blog.csdn.net/bmseven/article/details/131570524