数据结构-01-引言
本博文基于C语言的内容引入数据结构的时间、空间复杂度概念,并补充等差数列、累加和数列、等比数列等数学理论。
数据结构可以拆分为两个内容,即数据+结构。
是什么?
首先,数据从哪儿来?到哪儿去?
数据从生活各处中来,而目的是用数据驱动业务。
结构又是什么?
即数据的组织形式,比如数组、比如链表。
在特定的情境下,以往所用的数据类型无法基于需求合理地组织数据,此时需要自己设计一套新的数据组织形式来解决问题。
数据结构是一种存储、组织数据的方式,旨在方便访问和修改。任何一致数据结构都有各自的优劣。
C基础
函数
- 实现某个具体功能的代码块
- 增加代码复用性
- 降低编程难度
- 函数不被调用就不会执行
- 对内隐藏细节,对外暴露接口
基于需求对函数设定参数、设定返回值。
字符串
C语言中的字符串是按照字符数据进行存储的,并没有这种数据类型。
C#中有string类型
存储时字符串尾部带有
\0结束符可在定义时直接初始化赋值,也可在定义后使用
strcpy()赋值
1 | char arr1[11] = "HelloWorld"; |
地址
依据冯诺伊曼结构,计算机操作系统通常包括输入/输出设备,主存储器、辅存储器、控制器、运算器。
内存条、显卡、适配卡都各自有其存储地址空间。操作系统将设备的存储地址空间抽象为一个巨大的一维数组空间,如常说的32位/64位机。
数组
- 相同数据类型的集合
- 数组长度在定义时已经确定
- n元素的数组下标为0~n-1
1 | char a[11]; |
例如以上变量a:
- a:数组变量
- *a:数组首元素
再比如变量b:
- b:数组地址
- *b:数组首个行向量地址
- **b:数组首个行向量中首个元素地址
指针
指针与数据在某种程度上可以实现相同的功能,比如 :
1 | int arr[4] = {1,3,5,7}; |
则arr[i]和*(p+i)是一致的。指针在进行算术运算时,移动的是i与指针指向数据类型字节长度的乘积。
p+i的实际地址为p向后移动sizeof(int)*i。
| 有符号 | 无符号 | 32位 | 64位 |
|---|---|---|---|
| char | unsigned char | 1 | 1 |
| short | unsigned short | 2 | 2 |
| int | unsigned int | 4 | 4 |
| long | unsigned long | 4 | 8 |
| float | 4 | 8 | |
| double | 8 | 8 | |
结构体
结构体是一个或多个变量的集合,变量可以是不同的类型。
1 | struct point{ |
通常来说,结构体之间的传递也使用指针以提高效率。
1 | struct point *p; |
有些情况下,为了更清晰的表明结构体的功能,通常会使用别名:
1 | typedef unsigned int uint32_t; |
内存分配
静态/全局内存
静态声明的变量和全局变量使用这部分内存,程序开始运行时分配,程序结束才消失
自动内存(栈)
函数内局部变量,函数被调用时才创建
动态内存(堆)
根据需求写代码动态分配内存,需要程序主动释放。
数学基础
等差数列
假设有n项元素,其中a1至an中每两项之间的差值为d,则其前n项和
累加和数列
累加和数列是在等差数列的基础上再度累加。即
- b1 = S1 = a1
- b2 = S2 = a1+a2
- b3 = S3 = a1+a2+a3
- …
- bn = Sn = a1+a2+… + an = n(a1+an)/2
我们假设,数列b的前n项和为Tn,则有
利用求和公式计算得:
整理可得:
合并同类项得:
等比数列
数列{an}满足相邻项的比值为常数q,其通项式为 an = a1qn − 1
因此Sn的表达式为: Sn = a1 + a1q + a1q2 + ..a1qn − 1
两端同时成q以后减去上式: (q − 1)Sn = a1(qn − 1)
可得:
算法分析
- 时间复杂度
- 空间复杂度
- 抽象数据类型ADT
数据结构与算法存在本质联系。在研究某一类型的数据结构时,总要涉及其上施加的运算,只有通过对定义运算的研究,才能真正理解数据结构的定义与作用。
时间复杂度
时间复杂度,也称渐进复杂度,T(n) = O(f(n))。随问题规模n增大,算法执行时间增长率和f(n)增长率成正比。
程序运行时间主要和执行每条语句的耗时、每条语句的执行频率有关。
语句执行要由源程序翻译为目标代码,目标代码再装配执行。每个语句执行一次需要的具体时间和机器的软/硬件环节有关。因此,算法分析并非是实际执行时间,而是针对算法中语句执行次数做出估计,得到算法执行时间的信息。
例如:
1 | for(int i = 1; i<=n; i++) //循环n次,额外1次判断频度为n+1 |
上方代码的f(n) = n^3,当n=1时,f(n) = O(1)。
有以下区分:
- 最好时间复杂度:算法再最好情况下的时间复杂度
- 最坏时间复杂度:算法在最坏情况下的时间复杂度
- 平均时间复杂度:算法在所有可能的情况下,按输入实例等概率出现时的加权平均值
通常只讨论最坏时间复杂度。
常量阶
1 | x++; //频度为1 |
f(n) = 1+1;复杂度为O(1)。
1 | for(int i=0; i<10000; i++) |
算法执行时间不随问题的规模增长而增长,尽管执行了上万次,其仍然是常量。
线性阶
1 | for(int i=0; i<n; i++) |
f(n)=(n+1)+n+n = 3n+1,即复杂度T(n) = O(n)。
平方阶
1 | x++; //频度为n |
f(n)=(n+1)*n = n^2+n,即复杂度T(n) = O(n^2)。
立方阶
1 | x=1; |
f(n)=n*(n*(n+1)/2) = n^2+n,即复杂度T(n) = O(n^2)。
对数阶
1 | for(int i=0; i<=n; i=i*2) |
| 次数 | 1 | 2 | 3 | 4 | t |
|---|---|---|---|---|---|
| i | 1 | 2 | 4 | 8 | |
| i | 2^0 | 2^1 | 2^2 | 2^3 | 2^(t-1) |
2^t-1 > n时循环退出,可得t>log2n + 1
即T(n) = O(log2n),故也可以推导出倍数为其他值时的时间复杂度。
题目
1.例如:设n是描述问题规模的非负整数,下面程序片段的时间复杂度是();
1 | x = 2; |
实现复杂度的核心目的是找到语句的执行频次,也即什么时候退出循环,把每次x的值列出
| 次数 | 1 | 2 | 3 | 4 | t |
|---|---|---|---|---|---|
| i | 2^2 | 2^3 | 2^4 | 2^5 | 2^t+1 |
即2t+1 > n/2,可得t>log2n - 2,时间复杂度为O(log2n)。
2.再如:
1 | int fun(int n) |
同样的思路,列i与sum的变化表格:
| 次数 | 1 | 2 | 3 | 4 | t |
|---|---|---|---|---|---|
| i | 1 | 2 | 3 | 4 | t |
| sum | 1 | 1+2 | 1+2+3 | 1+..+4 | t(t+1)/2 |
即t(t+1)/2 >= n,推知t > n1/2,时间复杂度为O (n1/2)。
此处曾出现一处误解,即在t次将sum的值计算为n(n+1)/2 > n,从而得出复杂度为O(n)。根本原因在于将循环次数与问题规模混用一个n值。
3.再如:
1 | int sum = 0; |
| 次数 | 1 | 2 | 3 | 4 | t |
|---|---|---|---|---|---|
| i | 2^0 | 2^1 | 2^2 | 2^3 | 2^(t-1) |
| 内层次数 | 2^0 | 2^1 | 2^2 | 2^3 | 2^(t-1) |
其中外层函数的执行次数为2^(t-1) >=n ,可得t>log2n + 1。
而内层每次执行执行的次数与外圈i相等,故最内层语句总执行频度为:
空间复杂度
空间复杂度主要描述某个算法对应的程序想在计算机上执行,除去用来存储代码和输入数据的内存空间外,还需要额外的空间。
S(n) = O(f(n))。
抽象数据类型ADT
ADT是一种编程概念,用于定义数据类型及其操作,不涉及实现细节。优势是对外隐藏细节,对内隐藏接口。
在C中,ADT通常通过结构体和函数实现。
————————— End —————————
算法 + 数据结构 = 程序