数据结构初识,与算法复杂度

数据结构初识,与算法复杂度

前言

哎呀,前前后后,里里外外,我拖拖沓沓了快四个月,终于学完了C语言。

我只明白了一件事:学习,一定要多敲代码,一定要找一个安静(最好无人)的地方,一定要排除万难,一切为学习让步。

现在是2025年10月25日,16时6分,不说了,开干。

1、数据结构与算法

数据结构,是计算机存储、组织数据的方式,方式包括数据的增、删、查、改。

数据结构,指相互之间存在一种或多种特定关系的数据元素的集合。

算法,指的是一系列计算过程,用于将输入的一个(或一组)数据,经这个计算过程,输出一个(或一组)数据(结果)。

2、数据结构与算法的重要性

非常重要!非常重要!非常重要!

所以要好好学。

3、复杂度

3.1、复杂度的概念

我们知道,衡量一个算法的好坏,一般是从时间和空间两个维度出发。

即时间复杂度,和空间复杂度。

时间复杂度 空间复杂度
衡量一个算法的运行快慢 衡量一个算法所需要的额外空间的多少

3.2、复杂度的重要性

一句话:节约时间、节省空间、降低成本。

对于笔试面试,和实际开发,都有帮助。

3.3、时间复杂度

算法的时间复杂度,是一个函数T(N),用于定量计算算法的运行时间。

那为什么,不直接跑一跑这个算法,然后记录从开始运行,到结束,这个真实时间,并拿来分析呢?有三点原因:

  1. 编译环境(编译器)的更新迭代,会导致同一段代码,实际运行时间不同。
  2. 机器的新旧,会导致同一段代码,实际运行时间不同。
  3. 要知道这个真实时间,必须先运行代码,而不能通过一种理论,来提前判别。

话说回来。我们知道,写出来的代码,被编译为二进制可执行指令,而程序执行的过程,就是CPU执行这些指令。其中,某一条指令可能被执行一次,也可能被执行多次。所以:

程序的执行时间 = 二进制指令执行时间 ∗ 执行次数 程序的执行时间=二进制指令执行时间*执行次数 程序的执行时间=二进制指令执行时间∗执行次数

举个例子:

c 复制代码
//计算++count被执行的次数
void Func(int N)
{
	int count = 0;
	for (int i = 0; i < N; ++i)
	{
		++count;
	}

	for (int i = 0; i < 2 * k; ++i)
	{
		++count;
	}

	int M = 10;
	while (M--)
	{
		++count;
	}
}

不难得出:
T ( N ) = N 2 + 2 ∗ N + 10 T(N)=N^2+2*N+10 T(N)=N2+2∗N+10

其中,N^2对==T(N)==的影响最大。

但实际上,如果单单是执行一条简单的指令,那运行时间就微乎其微了。而有些语句,随着输入参数的变化,它们执行的次数也会随之发生相应有规律的变化。因此,我们计算算法复杂度,只是想计算算法程序的增长量级,也就是当N不断变化(一般是增大)时T(N)的差别。

上面的例子中,我们不难看出,低阶项与常数项,对结果的影响很小,所以我们只需计算一个大概的执行次数。这时可以用大O的渐进表示法。

3.3.1、大O的渐进表示法

规则:

  1. 时间复杂度函数式T(n)中,保留最高阶项。比如O(N^2)
  2. 去掉最高阶项的常数系数。
  3. 如果函数式中只有常数项,为O(1)

3.3.2、几个例子

举几个我觉得不太好理解的例子。

例一:

c 复制代码
void Func1(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);
}

在这里:

  1. T(N) = M + N
  2. 时间复杂度:O(M + N)
    • M == N,时间复杂度可以为O(M) O(N)
    • M >> N,时间复杂度O(M)
    • M << N,时间复杂度O(N)

例二:

c 复制代码
const char* strchr (const char * str, char char)
{
	const char* p_begin = str;
	while (*p_begin != char)
	{
		if (*p_begin == '\0')
			return NULL;
		p_begin++; 
	}
	return p_begin;
}

在这里:

  1. 当要查找的字符串在第一位,则T(N) = 1
  2. 当要查找的字符串在中间位置,则T(N) = N/2
  3. 当要查找的字符串在末尾,则T(N) = N

所以:

  1. 最好情况(下界):O(1)
  2. 平均情况:O(N)
  3. 最坏情况(上界):O(N)

而大O的渐进表示法,一般关注上界,即最坏情况。

例三:

c 复制代码
//冒泡排序
void BubbleSort(int* a, int n)
{
	assert(a);
	for (size_t end = n; end > 0; --end)
	{
		int exchange = 0;
		for (size_t i = 1; i < end; ++i)
		{
			if (a[i-1] > a[i])
			{
				Swap(&a[i-1], &a[i]);
				exchange = 1;
			}
		}
		if (exchange == 0)
			break;
	}
}

现在是2025年10月28日12时59分

在这里:

  1. 当序列升序,O(N) = N
  2. 当序列降序:
n 的取值 对应交换操作执行的次数
1 0
2 1
3 2
4 3
... ...
n n - 1

不难得出,执行次数为N(N - 1)/2,时间复杂度为O(N^2)

例四:

c 复制代码
void func2(int n)
{
	int cnt = 1;
	while (cnt < n)
	{
		cnt *= 2;
	}
}

可以看出,

n = 2,执行次数为1

n = 4,执行次数为2

n = 8,执行次数为4

......

所以此时的T(N) = logN (这里底数不重要,可由换底公式换成任意底),时间复杂度为O(logN)

例五:

也就是函数递归算法的复杂度。

递归的复杂度 = 递归的次数 * 每次递归的时间复杂度

3.4、空间复杂度

3.4.1、概念浅析

由于函数运行所需的栈空间,在编译期间就已经开辟好了,所以空间复杂度主要讨论的是函数在运行时,申请的额外内存空间。

3.4.2、示例

例一:

c 复制代码
void BubbleSort(int* a, int n)
{
	assert(a);
	for (size_t end = n; end > 0; --end)
	{
		int exchange = 0;
		for (size_t i = 1; i < end; ++i)
		{
			if (a[i-1] > a[i])
			{
				Swap(&a[i-1], &a[i]);
				exchange = 1;
			}
		}
		if (exchange == 0)
			break;
	}
}

还是这个冒泡排序。不难看出,使用了常数个空间,空间复杂度为O(1)

例二:

c 复制代码
//计算阶乘的递归
long long Fac(int n)
{
	if (n == 0)
		return 1;
	else
		return n*Fac(n - 1);
}

函数进行了n次递归,而每次递归申请了常数个空间,所以空间复杂度为O(N)

相关推荐
DARLING Zero two♡4 小时前
【优选算法】D&C-Mergesort-Harmonies:分治-归并的算法之谐
java·数据结构·c++·算法·leetcode
louisdlee.5 小时前
树状数组维护DP——前缀最大值
数据结构·c++·算法·dp
minji...7 小时前
算法题 逆波兰表达式/计算器
数据结构·c++·算法·1024程序员节
循着风10 小时前
二叉树的多种遍历方式
数据结构·算法
.格子衫.15 小时前
022数据结构之树状数组——算法备赛
数据结构·算法·1024程序员节
Yupureki15 小时前
从零开始的C++学习生活 16:C++11新特性全解析
c语言·数据结构·c++·学习·visual studio
對玛祷至昏17 小时前
数据结构理论知识
数据结构·算法·排序算法
黄思搏17 小时前
红黑树 - Red-Black Tree 原理与 C# 实现
数据结构·1024程序员节
剑锋所指,所向披靡!17 小时前
数据结构的基本概念
数据结构