C++数据结构与算法_数据结构与算法概念_时间复杂度

文章目录

  • [第一章 数据结构与算法基本概念](#第一章 数据结构与算法基本概念)
    • [1.4 时间复杂度](#1.4 时间复杂度)
      • [1.4.1 统计时间增长趋势](#1.4.1 统计时间增长趋势)
      • [1.4.2 函数渐进上界](#1.4.2 函数渐进上界)
      • [1.4.3 推算方法](#1.4.3 推算方法)
      • [1.4.4 常见的时间复杂度](#1.4.4 常见的时间复杂度)
        • [1 常数阶O(1)](#1 常数阶O(1))
        • [2 对数阶O(logn)](#2 对数阶O(logn))
        • [3 线性阶 𝑂(𝑛)](#3 线性阶 𝑂(𝑛))
        • [4. 线性对数阶 𝑂(𝑛 log 𝑛)](#4. 线性对数阶 𝑂(𝑛 log 𝑛))
        • [5 平方阶 𝑂(n^2)](#5 平方阶 𝑂(n^2))
        • [6 指数阶𝑂(2^n)](#6 指数阶𝑂(2^n))
        • [7 阶乘阶 𝑂(𝑛!)](#7 阶乘阶 𝑂(𝑛!))

本文介绍数据结构与算法之时间复杂度。

第一章 数据结构与算法基本概念

1.4 时间复杂度

1.4.1 统计时间增长趋势

时间复杂度的分析统计不是算法的运行时间,而是算法运行时间随着数据量变大时的增长趋势。下面举例子说明什么是时间增长趋势。

cpp 复制代码
	// 算法 A 的时间复杂度:常数阶
	void algorithm_A(int n) {
		cout << 0 << endl;
	}
	// 算法 B 的时间复杂度:线性阶
	void algorithm_B(int n) {
		for (int i = 0; i < n; i++) {
			cout << 0 << endl;
		}
	}
	// 算法 C 的时间复杂度:常数阶
	void algorithm_C(int n) {
		for (int i = 0; i < 1000000; i++) {
			cout << 0 << endl;
		}
	}
  1. 算法 A 只有 1 个打印操作,算法运行时间不随着 𝑛 增大而增长。我们称此算法的时间复杂度为"常数阶"。
  2. 算法 B 中的打印操作需要循环 𝑛 次,算法运行时间随着 𝑛 增大呈线性增长。此算法的时间复杂度被称为"线性阶"。
  3. 算法 C 中的打印操作需要循环 1000000 次,虽然运行时间很长,但它与输入数据大小 𝑛 无关。因此 C的时间复杂度和 A 相同,仍为"常数阶"。

1.4.2 函数渐进上界

函数渐近上界

若存在正实数 𝑐 和实数 𝑛0 ,使得对于所有的 𝑛 > 𝑛0 ,均有 𝑇(𝑛) ≤ 𝑐 ⋅ 𝑓(𝑛) ,则可认为 𝑓(𝑛) 给 出了 𝑇(𝑛) 的一个渐近上界,记为 𝑇(𝑛) = 𝑂(𝑓(𝑛)) 。

例如:

cpp 复制代码
	void algorithm(int n) {
		int a = 1; // +1
		a = a + 1; // +1
		a = a * 2; // +1
		// 循环 n 次
		for (int i = 0; i < n; i++) { // +1(每轮都执行 i ++)
			cout << 0 << endl; // +1
		}
	}

设算法的操作数量是一个关于输入数据大小 𝑛 的函数,记为 𝑇(𝑛) ,则以上函数的操作数量为:

𝑇(𝑛) = 3 + 2𝑛

𝑇(𝑛) 是一次函数,说明其运行时间的增长趋势是线性的,因此它的时间复杂度是线性阶。我们将线性阶的时间复杂度记为 𝑂(𝑛) ,这个数学符号称为大 𝑂 记号(big‑𝑂 notation),表示函数 𝑇(𝑛) 的渐近上界(asymptotic upper bound)。

1.4.3 推算方法

  1. 忽略 𝑇(𝑛) 中的常数项。因为它们都与 𝑛 无关,所以对时间复杂度不产生影响。
  2. 省略所有系数。例如,循环 2𝑛 次、5𝑛 + 1 次等,都可以简化记为 𝑛 次,因为 𝑛 前面的系数对时间复杂度没有影响。
  3. 循环嵌套时使用乘法。总操作数量等于外层循环和内层循环操作数量之积,每一层循环依然可以分别
    套用第 1. 点和第 2. 点的技巧。
cpp 复制代码
	void algorithm(int n) {
		int a = 1;
		// +0(技巧 1) 
		a = a + n;
		// +0(技巧 1) 
		// +n(技巧 2) 
		for (int i = 0; i < 5 * n + 1; i++) {
			cout << 0 << endl;
		}
		// +n*n(技巧 3)
		for (int i = 0; i < 2 * n; i++) {
			for (int j = 0; j < n + 1; j++) {
				cout << 0 << endl;
			}
		}
	}

上面代码的时间复杂度统计:

𝑇(𝑛) = 2𝑛(𝑛 + 1) + (5𝑛 + 1) + 2 完整统计(‑.‑|||)

= 2𝑛2 + 7𝑛 + 3

𝑇(𝑛) = 𝑛2 + 𝑛 偷懒统计(o.O)

时间复杂度由 𝑇(𝑛) 中最高阶的项来决定。这是因为在 𝑛 趋于无穷大时,最高阶的项将发挥主导作用,其他项的影响都可以忽略。

1.4.4 常见的时间复杂度

𝑂(1) < 𝑂(log 𝑛) < 𝑂(𝑛) < 𝑂(𝑛 log 𝑛) < 𝑂(𝑛2 ) < 𝑂(2𝑛) < 𝑂(𝑛!)

常数阶 < 对数阶 < 线性阶 < 线性对数阶 < 平方阶 < 指数阶 < 阶乘阶

1 常数阶O(1)

常数阶的操作数量与输入数据大小 𝑛 无关,即不随着 𝑛 的变化而变化。在以下函数中,尽管操作数量 size 可能很大,但由于其与输入数据大小 𝑛 无关,因此时间复杂度仍为 𝑂(1)。

cpp 复制代码
	/* 常数阶 */
	int constant(int n) 
	{
		int count = 0;
		int size = 100000;
		for (int i = 0; i < size; i++)
			count++;
		return count;
	}
2 对数阶O(logn)

对数阶反映了 "每轮缩减到一半" 的情况。设输入数据大小为n, 由于每轮缩小到一半,因此循环次数为log2n,即2^n 相反数。

cpp 复制代码
	// 对数阶 循环实现
	int logarithmic(int n)
	{
		int count = 0;
		// 循环次数与数据大小n的对数成正比
		while (n > 1)
		{
			n /= 2;  // 这里体现了对数的特性
			++count;
		}
		return count;
	}

推倒上面代码的时间复杂度:

初始状态:当n开始时是一个正整数;

循环体:每次循环,n = n/2, 每次循环n减半。如果循环共执行了k次,那么n被除以2^k次。

循环终止条件:n <= 1,循环停止。

与指数类似,对数阶也常出现在递归函数中,以下代码形成了一颗高度为 log2 n的递归树。

cpp 复制代码
	int logRecur(int n)
	{
		// 递归终止条件
		if (n <= 1)
		{
			return 1;
		}
		// 递归调用
		return logRecur(n / 2) + 1;
	}

对数阶常出现于基于分治策略的算法中,体现了"一分为多"和"化繁为简"的算法思想。它增长缓慢,是仅次于常数阶的理想的时间复杂度。

3 线性阶 𝑂(𝑛)

线性阶的操作数量相对于输入数据大小 𝑛 以线性级别增长。线性阶通常出现在单层循环中:

cpp 复制代码
	/* 线性阶 */
	int linear(int n) {
		int count = 0;
		for (int i = 0; i < n; i++)
			count++;
		return count;
	}

遍历数组和遍历链表等操作的时间复杂度均为 𝑂(𝑛) ,其中 𝑛 为数组或链表的长度:

/* 线性阶(遍历数组) */

cpp 复制代码
	int arrayTraversal(vector<int> &nums) 
	{
		int count = 0;
		// 循环次数与数组长度成正比
		for (int num : nums) 
		{
			count++;
		}
		return count;
	}
4. 线性对数阶 𝑂(𝑛 log 𝑛)

线性对数阶常出现于嵌套循环中,两层循环的时间复杂度分别为 𝑂(log 𝑛) 和 𝑂(𝑛) 。

cpp 复制代码
	int linearRecur(int n)
	{
		if (n <= 1)
		{
			return 1;
		}

		int count = linearRecur(n / 2) + linearRecur(n / 2);
		for (int i = 0; i < n; ++i)
		{
			++count;
		}

		return count;
	}

下图展示了线性对数阶的生成方式。二叉树的每一层的操作总数都为 𝑛 ,树共有 log2𝑛 + 1 层,因此时间复杂度为 𝑂(𝑛 log 𝑛) 。

主流排序算法的时间复杂度通常为 𝑂(𝑛 log 𝑛) ,例如快速排序、归并排序、堆排序等。

5 平方阶 𝑂(n^2)

平方阶的操作数量相对于输入数据大小 𝑛 以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环的时间复杂度都为 𝑂(𝑛) ,因此总体的时间复杂度为 𝑂(𝑛2 ) :

cpp 复制代码
	int quadratic(int n)
	{
		int count = 0;
		// 循环次数与数据大小n的平方成正比
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < n; ++j)
			{
				++count;
			}
		}

		return count;
	}

以冒泡排序为例:外层循环执行 𝑛 − 1 次,内层循环执行 𝑛 − 1、𝑛 − 2、...、2、1 次,平均为 𝑛/2 次,因此时间复杂度为 𝑂((𝑛 − 1)𝑛/2) = 𝑂(𝑛2) :

cpp 复制代码
	int bubbleSort(vector<int>& nums)
	{
		int count = 0;
		// 外层循环 :未排序区间 [0 , i]
		for (int i = nums.size() - 1; i > 0; --i)
		{
			// 内循环:将[0,i]中的最大元素交换至该区间的最右端
			for (int j = 0; j < i; ++j)
			{
				if (nums[j] > nums[j + 1])
				{
					swap(nums[j], nums[j + 1]);
				}
				++count;
			}
		}
	}
6 指数阶𝑂(2^n)

生物学的"细胞分裂"是指数增长的典型例子:初始状态是1个细胞,分裂一轮后为2个细胞,分裂两轮后4个细胞,分裂n轮后为2^n个细胞。

下面代码模拟细胞分裂过程,时间复杂度为O(2^n).

cpp 复制代码
	// 指数阶
	int exponential(int n)
	{
		int count = 1;
		int base = 1;
		// 循环次数与数据大小n的指数成正比
		for (int i = 0; i < n; ++i)
		{
			for (int j = 0; j < base; ++j)
			{
				++count;
			}
			base *= 2;
		}
		// count = 1 +  2 + 4 + 8 + ... + 2^(n-1) = 2^n - 1
		return count;
	}

等比数列前n项和。

计算方法是:等比数列前n项和,Sn=a1(1-q^n)/(1-q)

在实际中,指数阶常出现在递归函数中,在下面代码中,递归地一分为二,经过n次分裂后停止:

cpp 复制代码
	int expRecur(int n)
	{
		// 递归终止条件
		if (n == 1)
		{
			return 1;
		}
		// 递归调用
		return expRecur(n - 1) + expRecur(n - 1);  // 表示每层的数量  比如,expRecur(3) = 8 expRecur(2) = 4
		return expRecur(n - 1) + expRecur(n - 1) + 1; // 表示总的数量,比如 expRecur(3) = 15 expRecur(2) = 7
	}

指数阶增长非常迅速,在穷举法(搜索,回溯)中比较常见。对于规模较大的问题,指数阶次是不可接收的,通常使用动态规划和贪心算法解决。

7 阶乘阶 𝑂(𝑛!)

阶乘阶对应数学上的"全排列"问题。给定 𝑛 个互不重复的元素,求其所有可能的排列方案,方案数量为:

𝑛! = 𝑛 × (𝑛 − 1) × (𝑛 − 2) × ⋯ × 2 × 1

阶乘通常使用递归实现。如图 2 14 和以下代码所示,第一层分裂出 𝑛 个,第二层分裂出 𝑛 − 1 个,以此类推,直至第 𝑛 层时停止分裂:

cpp 复制代码
	int factorRecur(int n)
	{
		if (n == 0)
		{
			return 1;
		}

		int count = 0;

		// 从1个分裂出n个
		for (int i = 0; i < n; ++i)
		{
			count += factorRecur(n - 1);
		}
		return count;

	}

请注意,因为当 𝑛 ≥ 4 时恒有 𝑛! > 2𝑛 ,所以阶乘阶比指数阶增长得更快,在 𝑛 较大时也是不可接受的。

相关推荐
天赐学c语言3 小时前
12.11 - 最长回文子串 && main函数是如何开始的
c++·算法·leetcode
deng-c-f3 小时前
C/C++内置库函数(4):c++左右值及引用的概念、move/forward的使用
c语言·开发语言·c++
图形学爱好者_Wu3 小时前
每日一个C++知识点|原子操作
c++·编程语言
特立独行的猫a3 小时前
C++观察者模式设计及实现:玩转设计模式的发布-订阅机制
c++·观察者模式·设计模式
deng-c-f3 小时前
C/C++内置库函数(3):future、promise的用法
c语言·开发语言·c++
deng-c-f3 小时前
C/C++内置库函数(6):C++中类什么时候使用静态变量
开发语言·c++
2301_789015623 小时前
C++:模板进阶
c语言·开发语言·汇编·c++
是小胡嘛4 小时前
仿Muduo高并发服务器之Buffer模块
开发语言·c++·算法
im_AMBER4 小时前
Leetcode 75 数对和 | 存在重复元素 II
c++·笔记·学习·算法·leetcode