数据结构与算法01:时间复杂度

目录

【复杂度分析】

【降低时间复杂度】

降低时间复杂度的必要性

【每日一练】


不管是使用什么编程语言或者哪种数据库,不管是解决项目中的什么问题,都离不开数据结构与算法。所谓数据结构就是指某一种数据的存储结构,所谓算法就是操作这种数据的方式方法。举个生活中的例子,比如查字典,字典中的所有内容就可以理解为是一种数据结构,而如何找到想要的字就需要算法来处理。比如软件开发中常用的MySQL、Redis以及各类编程语言中都用到了大量的数据结构和算法。熟悉各类数据结构存储和算法能够编写出高效的程序代码。

作为开发人员,一般比较常用的数据结构有:数组、链表、堆、栈、队列、树、跳表、图;常用的算法有:递归、排序、查找、贪心算法、哈希算法、分治算法、深度优先、广度优先、动态规划等。

线性表:如果一组数据结构中的数据排成像一条线一样,那么就是线性表,每个线性表上的数据最多只有前和后两个方向(指针)。数组、链表、队列、栈 都是线性表结构

非线性表:数据之间并不是简单的前后关系,二叉树、堆、图 都是非线性表结构。

【复杂度分析】

复杂度分为“时间复杂度”和“空间复杂度”,主要用来估算某种数据结构或者算法的执行效率,表示的是一个算法执行效率与数据规模增长的变化趋势,一般使用大O来表示。常见的复杂度量级如下(按照数量级递增):

复杂度表示说明
O(1)常数级别
O(log(n))对数级别
O(n)线性级别
O(n * log(n))线程对数级别
O(n^2)、O(n^3)、O(n^k)平方级别、立方级别、k次方级别
O(2^n)

指数级别  

O(n!)

阶乘级别  

复杂度的细节说明:

  • O(1) :是常量级时间复杂度的表示方法,并不是只执行了1行代码;如果某段代码执行了4行,那么它的时间复杂度也是 O(1),而不是 O(4)。只要程序算法中没有循环语句和递归语句,即使有上万行代码,那么时间复杂度也是Ο(1)。只要是顺序结构的代码,时间复杂度基本都是O(1)。
  • O(logn) 和 O(n * logn):对数级别,一般会忽略对数的底数,不管是O(log2n) 还是 O(log3n) 都统一表示为O(log(n))。如果对O(log(n))的循环执行 n 遍,时间复杂度就是 O(n * log(n)) ,归并排序 和 快速排序 的时间复杂度就是 O(n * log(n)),一般简写为 O(nlogn)。一般出现二分查找或者分而治之策略的时候,时间复杂度基本都是O(logn)。
  • O(m+n) 和 O(m*n):这种情况的复杂度由m和n两个数据的规模来决定。
  • 关于循环:只关注循环执行次数最多的一段代码,一般会忽略掉常量、低阶、系数,只需要记录一个最大阶的量级就可以了,比如O(n * 2)和O(n)是一样的。如果是k层for循环,那么时间复杂度就是O(n^k)。
  • 关于加法:总复杂度等于量级最大的那段代码的复杂度,因为当变量n无限大的时候,基本上只需要关注最大量级的代码的计算了,比如O(n^2) + O(n) 也就相当于O(n^2)了。
  • 关于乘法:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积,比如嵌套循环了3次,那么复杂度就需要对3次嵌套的复杂度相乘。
package main

import "fmt"

func test1() {
	// O(1) 常量级别复杂度
	var a = 5
	var b = 3
	var c = 1
	fmt.Println(a + b + c)

	// O(logn) 和 O(n * logn) 对数级别
	var d = 1
	for d <= 10 {
		// 变量d的值从1开始取,每循环一次就乘以2;当大于10时循环结束,变量d的取值是一个等比数列。
		// 所以这段代码的时间复杂度是 O(log2n),注意这个2是下标2,输入法打不出来.
		d = d * 2
	}
	fmt.Println(d)

	// O(m+n) :这种情况的复杂度由m和n两个数据的规模来决定
	fmt.Println(test2(100, 300))
}

func test2(m, n int) int {
	// 无法事先评估 m 和 n 谁的量级大,所以不能利用加法法则省略掉其中一个;
	// 所以这段代码的时间复杂度就是 O(m+n)
	var sum1 = 0
	for i := 1; i < m; i++ {
		sum1 = sum1 + i
	}

	var sum2 = 0
	for j := 1; j < n; j++ {
		sum2 = sum2 + j
	}

	return sum1 + sum2

}

func main() {
	test1()
}

时间复杂度与代码的运算方式有关系,空间复杂度与数据结构的设计有关系。比如需要对一个数组的元素逆序输出,可以有下面两种方式:

// 逆序输出数组的元素,方法1:逐个遍历并逆序赋值
// 时间复杂度是O(n),空间复杂度是O(n)
func reverseArray1(arr [5]int) [5]int {
	var newArr = [5]int{}
	for i := 0; i < len(arr); i++ {
		newArr[len(arr)-i-1] = arr[i]
	}
	return newArr
}

// 逆序输出数组的元素,方法2:前后互相调换
// 时间复杂度是O(n/2),也就是O(n),空间复杂度是O(1)
func reverseArray2(arr [5]int) [5]int {
	var tmp = 0
	for i := 0; i < len(arr)/2; i++ {
		tmp = arr[i]
		arr[i] = arr[len(arr)-i-1]
		arr[len(arr)-i-1] = tmp
	}
	return arr
}

func main() {
	fmt.Println(reverseArray1([5]int{1, 2, 3, 4, 5})) //[5 4 3 2 1]
	fmt.Println(reverseArray2([5]int{1, 2, 3, 4, 5})) //[5 4 3 2 1]
}

上面两种方法输出的结果都是正确的,但是第二种的空间复杂度是常数1,因此效率更高。

【降低时间复杂度】

降低时间复杂度的必要性

实际的生产环境中,用户的访问请求可以看作一个流式数据,假设这个数据流中每个访问的平均时间间隔是t,如果代码无法在 t 时间内处理完单次的访问请求,那么这个系统最终被大量积压的任务给压垮,这就要求开发人员必须通过优化代码来降低时间复杂度。

数据量小的时候,不管怎么写程序,运行的结果差别都不会太大;但是如果数据量特别大的情况下,不同的时间复杂度运行的结果可能千差万别。假设某个计算任务需要处理10万条数据,使用不同的时间复杂度的结果如下:

  • 如果是O(n^2)的时间复杂度,那么计算的次数就是 10万*10万 = 100亿次;
  • 如果是O(n)的时间复杂度,那么计算的次数就是10万次;
  • 如果是O(logn)的时间复杂度下,那么计算的次数就是17次左右(log 100000 = 16.61,这里的对数以2为底去估计)。

降低时间复杂度的方法:

(1)空间换时间:假设一段程序在比较低配置的计算机上运行可能需要很长时间,那么就可以花钱购买高配置或者更多的服务器来缩短运行时间,也就是俗话说的“堆机器”。这样的操作降低了时间复杂度,但增加了空间复杂度。但实际上空间(云服务器)是比较廉价的,而时间是很宝贵的,你总不能让用户打开一个页面等待好几分钟吧?

(2)通过程序算法降低时间复杂度,常用的算法有:递归、二分法、排序算法、动态规划等等。

程序优化的核心思路:

  • 暴力解法:在没有任何时间、空间约束下,完成代码任务的开发;
  • 无效操作处理:将代码中的无效计算、无效存储剔除,降低时间或空间复杂度;
  • 时空转换:设计合理数据结构,完成时间复杂度向空间复杂度的转移;

【每日一练】

力扣:两数之和(https://leetcode.cn/problems/two-sum/

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例:输入:nums = [2,7,11,15], target = 9,输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

思路1:暴力解法,双重循环遍历。时间复杂度: O(n^2),空间复杂度: O(1)

func twoSum1(nums []int, target int) []int {
	// 第一轮遍历
	for i := 0; i < len(nums); i++ {
		// 第二轮遍历不能重复计算了
		for j := i + 1; j < len(nums); j++ {
			if nums[i]+nums[j] == target {
				// 注意 leetcode 中要求返回的是索引位置
				return []int{i, j}
			}
		}
	}
	return []int{}
}

func main() {
	fmt.Println(twoSum1([]int{2, 7, 11, 15}, 22)) //[1 3]
}

思路2:空间换时间,使用map(键值对)存储。时间复杂度: O(n),空间复杂度: O(n)

func twoSum2(nums []int, target int) []int {
	find := map[int]int{}
	for j, num := range nums {
		if i, ok := find[target-num]; ok {
			return []int{i, j}
		}
		// 每一轮都存下当前num和对应的index到map中
		find[num] = j
	}
	return []int{}
}

func main() {
	fmt.Println(twoSum2([]int{2, 7, 11, 15}, 22)) //[1 3]
}

源代码:https://gitee.com/rxbook/go-algo-demo/blob/master/algo01/demo1.go

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浮尘笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值