初识数据结构与算法

目录

[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. 集合:结构中的元素仅满足在同一个集合中。

  2. 线性:结构中的元素是一种先后关系,对其中任意结点,与它相邻且在前面的结点(前驱节点)最多有一个,与它相邻且在后面的结点(后继结点)最多只有一个。

  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)及以上时,效率就不太令人满意了。


总结

以上简单介绍了数据结构与算法有关内容,关于数据结构其余内容,请期待后续更新


以上内容如有错误或不准确之处,欢迎指出,或者你有更好的想法,也欢迎交流。

相关推荐
naturerun5 小时前
从数组中删除元素的算法
数据结构·c++·算法
酿情师8 小时前
区块链原理与技术02:区块链的数据结构04(区块结构)
数据结构·区块链
夏日听雨眠8 小时前
数据结构(循环队列)
数据结构·算法·链表
平行侠8 小时前
30MacLaren-Marsaglia算法故事文件
数据结构·算法
平行侠10 小时前
33水库抽样 - 从未知大小的流中等概率采样
数据结构·算法
Controller-Inversion10 小时前
42. 接雨水
数据结构·算法·leetcode
Controller-Inversion10 小时前
33. 搜索旋转排序数组
数据结构·算法·leetcode
宵时待雨11 小时前
优选算法专题6:模拟
数据结构·c++·算法·leetcode·职场和发展