什么是数据结构 ?
其实官方没有统一定义!!!
“数据结构是数据对象,以及存在于该对象的实例和组成实例的数据元素之间的各种联系。这种联系可以通过定义相关的函数给出。” - 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-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)+n∗n,可以用一个函数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=264−1
据专辑统计,每一麦粒的平均重量约 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 的增长,这个算法会不会“爆掉”?
常见的算法时间复杂度分类:
- 常数阶:运行的次数是一个常数,如 5、20、100。常数阶算法时间复杂度通常用 O ( 1 ) O(1) O(1)表示。
- 多项式阶:很多算法时间复杂度是多项式,通常用 O ( n ) 、 O ( n 2 ) 、 O ( n 3 ) O(n)、O(n^2)、O(n^3) O(n)、O(n2)、O(n3)等表示。
- 指数阶:指数阶时间复杂度运行效率极差,程序员往往像躲“恶魔”一样避开它。常见的有 O ( 2 n ) 、 O ( n ! ) 、 O ( n n ) O(2^n)、O(n!)、O(n^n) O(2n)、O(n!)、O(nn)等。使用这样的算法要慎重,例如上面的趣味故事。
- 对数阶:时间复杂度运行效率较高,常见的有 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(n−2)+f(n−1),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 教授曾提出:数据结构+算法=程序。
数据结构与算法之间存在着密切的关系。算法是一种解决问题的方法或步骤,它们通过操作数据结构来实现。算法可以实现各种功能,如插入、排序、搜索、图遍历等。不同的算法对于不同的问题具有不同的时间复杂度和空间复杂度。因此,选择合适的数据结构和算法对程序的性能至关重要。
数据结构与算法的重要性:
-
它们可以提高程序的执行效率。通过选择适当的数据结构和算法,我们可以降低时间和空间的消耗,减少程序的运行时间。这对于大规模数据处理和复杂应用程序来说尤为重要。
-
它们有助于提高代码的可读性和可维护性。良好的数据结构和算法设计可以使代码更加清晰和易于理解。这可以帮助开发人员更好地理解代码逻辑,减少出错的可能性,并且方便代码的维护和拓展。
-
数据结构与算法也是衡量编程能力的重要指标之一。熟练掌握数据结构与算法的知识和技巧,可以提高我们解决实际问题的能力,并且在面试和竞争中具备一定的优势。
们可以提高程序的执行效率。通过选择适当的数据结构和算法,我们可以降低时间和空间的消耗,减少程序的运行时间。这对于大规模数据处理和复杂应用程序来说尤为重要。 -
它们有助于提高代码的可读性和可维护性。良好的数据结构和算法设计可以使代码更加清晰和易于理解。这可以帮助开发人员更好地理解代码逻辑,减少出错的可能性,并且方便代码的维护和拓展。
-
数据结构与算法也是衡量编程能力的重要指标之一。熟练掌握数据结构与算法的知识和技巧,可以提高我们解决实际问题的能力,并且在面试和竞争中具备一定的优势。