目录
[1 · 数据结构](#1 · 数据结构)
[1 - 1 · 什么是数据结构](#1 - 1 · 什么是数据结构)
[1 - 2 · 为什么需要数据结构](#1 - 2 · 为什么需要数据结构)
[1 - 3 · 数据的逻辑结构](#1 - 3 · 数据的逻辑结构)
[1 - 4 · 数据的存储结构](#1 - 4 · 数据的存储结构)
[2 · 算法](#2 · 算法)
[2 - 1 · 算法的评价](#2 - 1 · 算法的评价)
[3 · 时间复杂度](#3 · 时间复杂度)
[3 - 1 · 时间复杂度概念](#3 - 1 · 时间复杂度概念)
[3 - 2 · 大O渐近表示法](#3 - 2 · 大O渐近表示法)
[3 - 3 · 举几个例子](#3 - 3 · 举几个例子)
[4 · 空间复杂度](#4 · 空间复杂度)
[5 · 常见复杂度](#5 · 常见复杂度)
1 · 数据结构
1 - 1 · 什么是数据结构
简单来说,
数据结构 (Data Structure) 是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的
数据元素的集合。
展开来说,
数据结构是由"数据"和 "结构"两个词组合起来的,
什么是数据?常见的数值1、2、3、4.....、教务系统里保存的用户信息(姓名、性别、年龄、学历等等)、⽹页里肉眼可以看到的信息(文字、图片、视频等等),这些都是数据。
什么是结构?
当我们想要使用大量使用同⼀类型的数据时,通过手动定义大量的独立的变量对于程序来说,可读性非常差,我们可以借助数组这样的数据结构将大量的数据组织在一起,结构也可以理解为组织数据的方式。
1 - 2 · 为什么需要数据结构
程序中如果不对数据进行管理,可能会导致数据丢失、操作数据困难、野指针等情况。
通过数据结构,能够有效将数据组织和管理在一起。按照我们的方式任意对数据进行增删改查等操
作。
我们之前已经介绍过数组,其实数组就是一个基础的数据结构。
但是仅仅有数组,其实是不满足我们的需求的:
假定数组有10个空间,已经使用了5个,向数组中插入数据步骤:
求数组的长度,求数组的有效数据个数,向下标为数据有效个数的位置插入数据(并且这里要判断数组是否满了)
假设数据量非常庞大,频繁的获取数组有效数据个数会影响程序执行效率。
1 - 3 · 数据的逻辑结构
根据数据元素之间的逻辑关系的不同特性,大致可分出4中基本逻辑结构:
集合结构,线性结构,树形结构,图形结构。
-
集合:结构中的元素仅满足在同一个集合中。
-
线性:结构中的元素是一种先后关系,对其中任意结点,与它相邻且在前面的结点(前驱节点)最多有一个,与它相邻且在后面的结点(后继结点)最多只有一个。
-
树形 : 结构中的元素存在"多对一"的关系,一个数据元素向上与一个数据元素相连(双亲结点),向下与多个数据元素相连(孩子结点)。
-
图形(网状) : 结构中的任意元素之间都可以有关系。
1 - 4 · 数据的存储结构
数据的逻辑结构在计算机存储器的实现,就是数据的存储结构,也称为物理结构。
下面简单介绍一下:
1 . 顺序存储 : 借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系。在C语言中,最简单的方法就是用一维数组来实现。
2 . 链式存储 : 借助指示元素存储地址的指针来表示数据元素间的逻辑关系。
3 . 索引存储 : 在原有存储结构的基础上,附加建立一个索引表,索引表由关键字和地址组成,反映了按某一个关键字递增或递减的逻辑次序,主要作用是提高检索速度。
4 . 散列存储 : 通过构造散列函数来确定数据存储地址或查找地址。
2 · 算法
算法 (Algorithm): 就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
2 - 1 · 算法的评价
如何衡量一个算法的好坏呢?
一个好的算法首先要具备正确性,可读性,以及健壮性:
正确性:算法能够正确执行预期的功能。
可读性:为了便于理解,修改,测试代码,好的算法需要具有良好的可读性。
健壮性:当输入非法的数据时,算法可以作出反应或进行相应处理,而不是产生一个莫名其妙的结果。
那么下面我们看一个求斐波那契数列的算法:
long long Fib(int N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
用递归的方法来实现斐波那契数列,写出来的代码很简洁,但我们运行之后也知道:哪怕仅仅给N传50,最后也要等很久才能得到结果。
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般 是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
3 · 时间复杂度
3 - 1 · 时间复杂度概念
在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。
一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。那难道要我们将每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。
一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法
的时间复杂度。
所以算时间复杂度,其实是有关问题规模N 的函数的极限情况。
3 - 2 · 大O渐近表示法
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这 里我们使用大O的渐进表示法。
大O符号(O), 是用于描述函数渐进行为的数学符号
大O渐进表示法其实类似于找数量级。
比如 T(n) = 3N^2 + 2N + 100 ,此时这个表达式的数量级与 N^2 相同,因此 T(N) = O(N^2)。
当表达式为多项式时,只需保留其最高阶项的N,并省略其系数,其余次阶项以及常数项均省略。
当表达式为常数时,说明此时算法与问题规模N无关,此时时间复杂度就是 O(1)。
时间复杂度的数量级越大,说明该算法的效率越慢。
所以大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
当然你可能会有疑惑:对于 T(N) = N^2 + N + 1 和 Q(N) = 2N + 1000 ,T(N)的时间复杂度是O(N^2),而Q(N)的时间复杂度是O(N),但是当 N等于10的时候,这两个函数对应的算法需要执行的语句次数分别为 111 和 1020 次,此时显然是后者更快。
但实际上,当所需执行次数较小时,我们认为效率是相等的,因为CPU主频(CPU每秒钟可以完成的时钟周期数)运算速度是很快的,每秒可以执行上亿次。
所以在比较时,我们一般将N 看作趋近于无限,此时实际执行的次数自然也就与次阶项与常数项关系不大了。
并且有的时候算法存在最好、平均和最坏情况:
比如在一个长度为N数组中搜索一个数据x
最好情况:1次找到。
平均情况:N/2 次找到。
最坏情况:N次找到。
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
3 - 3 · 举几个例子
void Test(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10000;
while (M--)
{
++count;
}
printf("%d\n", count);
}
执行次数是 2N + 10000 ,因此时间复杂度是 O(N)。
void Func3(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++ k)
{
++count;
}
for (int k = 0; k < N ; ++ k)
{
++count;
}
printf("%d\n", count);
}
这里有两个未知数,执行次数是 N + M 次,所以时间复杂度是 O(N+M),但如果已知 N 远大于M,那么时间复杂度就是 O(N)。当然,如果已知 M 远大于 N ,时间复杂度就是 O(M)。
void BubbleSort(int* arr, int sz)
{
int i = 1;
int j = 0;
int t = 0;
int flag = 1;
//趟数
for (i = 1; i <= sz - 1; i++)
{
int flag = 1;//判断是否提前排序完成
//一次确定一个
for (j = 0; j <= sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
//从小到大排,前者大就交换
t = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = t;
//如果发生交换,说明还在进行排序
flag = 0;
}
}
//如果一趟下来没发生交换,说明已排序完成
if (flag)
{
break;
}
}
}
冒泡排序,最坏情况是遇到了逆序,此时执行次数满足等差数列,用求和公式可得 (N*(N+1)/2),时间复杂度是O(N^2)。
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
while (begin <= end)
{
int mid = begin + ((end-begin)>>1);
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid-1;
else
return mid;
}
return -1;
}
二分查找,每次排除一半,最坏情况是最后分出一个大小为1的区间才找到或者没找到。
每循环一次相当于除2,那么我们可以列个式子:
N/2/2......./2 = 1
假设找了 x 次,
那么就有 2^x = N , 此时x 等于 log 以2为底, N 的对数,时间复杂度为 O(logN)。
**注意:**当数量级为 log 以2为底, N 的对数时,大O渐进表示法log 底下的2是可以省略的,如果是以其他数为底就不能省。
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
函数递归了N次,执行N次,时间复杂度为 O(N)
4 · 空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度。
空间复杂度不是程序占用了多少的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因 此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
5 · 常见复杂度
常见的复杂度的大小次序如下:
O(1) < O(logN) < O(N) < O(NlogN) < O(N^2) < O(N^3) < O(2^N)
其中,当复杂度到达 O(N^3)及以上时,效率就不太令人满意了。
总结
以上简单介绍了数据结构与算法有关内容,关于数据结构其余内容,请期待后续更新
以上内容如有错误或不准确之处,欢迎指出,或者你有更好的想法,也欢迎交流。