基础算法-1 时间复杂度和简单排序及二分法

本课程是牛客网 左程云的课程总结

  1. 时间复杂度

    常数时间的操作: 一个操作是固定时间的,与样本量没有关系,每次都是固定时间内完成的操作。
    比如 数组的寻址操作(通过偏移量来获取),位运算(左移,右移,或,异或等)
    非常数时间操作: 比如 list 类型 获取第i个位置的值,这里只能一个个遍历,遍历到第i个位置 取出对应的值。

    时间复杂度是一种标准,粗略描述了常数时间操作的数量级。
    时间复杂度为一个算法流程中,常数操作数量的一个指标,常用O来表示,具体来说,先要对一个算法流程非常熟悉,然后去写出这个算法流程中,发生了多少常数操作,进而总结出常数操作数量的表达式。

    在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果f(N), 那么时间复杂度为O(f(N)).

    评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是“常数时间”。(拼常数时间时,直接生成大量数据进行实践操作,比如算数运行没有位运算快)

例子:

//选择排序算法,时间复杂度的计算。
'''
共6个数,每次选择未排序的所有数中最小的数
原始数据: 5,2,4,1,0,3
第一次:遍历6个数   0 (5,2,4,1,3)
第二次:遍历5个数   0,1 (5,2,4,3)
第三次:遍历4个数   0,1,2 (5,4,3)
第四次:遍历3个数   0, 1,2, 3(5,4)
第五次:遍历2个数   0,1,2, 3, 4(5)
第六次:遍历1个数   0,1,2, 3, 4, 5
总共排序6次,遍历次数为 n + (n-1) + (n-2) + (n-3) + ...+ 1 
上式为等差数列

等差数列 第n项 为 a_n = a_1 + (n-1)*d (d为公差)
等差数列 求和公式 为 Sn = n*a_1 + (n(n-1)/2)*d = n^2 * d/2 + (a_1 -d/2)*n

时间复杂度: 只要高阶项,不要低阶项,也不要高阶项的系数。所以选择排序时间复杂度为O(n^2)
'''

//题目: 给定一个数组返回每一个位置左右最小值。
'''
共6个数, 值为 5,2,4,1,0,3  

方法一: 最简单的方法是暴利查询每个位置左侧和右侧最小值。查询次数 n*(n-1)(共n个数,每个位置需要查询左右侧n-1个数,所以n*(n-1)),时间复杂度为O(n^2)

方法二: 通过2个辅助数组。
数组A: 从左到右遍历原始数组生成 辅助数组A,只降不增的数组 [5,2,2,1,0,0] 
      这样就得到0->i位置上的最小值。
      0-0 最小值 5,
      0-1 最小值 2,
      0-2 最小值 2,
      0-3 最小值 1,
      0-4 最小值 0,
      0-5 最小值 0
数组B:再从右到左遍历一遍原始数组生成 辅助数组B, [0,0,0,0,0,3] 得到i->(n-1)上的最小数组。
	0-5 最小值 0,
	1-5 最小值 0,
	2-5 最小值 0,
	3-5 最小值 0,
	4-5 最小值 0,
	5-5 最小值 3
这样两个数组 每个位置对应的值即为每个位置左侧和右侧的最小值。
(5,0)(2,0)(2,0)(1,0)(0,0)(0,3)

时间复杂度: O(n) + O(n) + O(1) = O(n)
额外空间复杂度:O(n)
'''
  1. 选择排序

    时间复杂度 O(n^2)

def selectSort(data):
	if len(data) < 2:
		return
	for i in range(0,len(data)-1):
		for j in range(i+1, len(data)):
			if data[j] < data[i]:
				data[i],data[j] = data[j],data[i]
  1. 冒泡排序
    时间复杂度 O(n^2)
def bubbleSort(data):
	if len(data) < 2:
		return data
	for i in range(len(data)-1, -1, -1):
		for j in range(0, i):
			if data[j] > data[j+1]:
				data[j], data[j+1] = data[j+1], data[j]

  1. 插入排序

    时间复杂度是 O(n^2)
    有序的部分 就像手里抓好的牌,新抓的牌 比较大小 插入已有的牌中

def insertSort(data):
	for i in range(1, len(data)):
		// 0-i上有序
		for j in range(i-1, -1, -1):
			if data[j+1] < data[j]:
				data[j+1], data[j] = data[j],data[j+1]

从插入排序开始,时间复杂度 和 数据状况 有关系。
如果刚开始,数据已经排好,则O(N)
如果刚开始,数据逆序,则O(N^2)
按照最差的情况,来估计时间复杂度。

  1. 二分法详细和扩展

    题目1: 在一个有序数组中,找某个数是否存在
    题目2: 在一个有序数组中,找>=某个数最左的位置
    题目3: 局部最小值问题


// 题目1:O(logN) 
def findK(data, K):
	if len(data) < 1:
		return False
	def findK_func(data, left, right, K):
		if right > left:
			if K < data[left] or K > data[right]:
				return False
			mid = (right - left)//2 + left
			if K == data[mid]:
				return True
			else if K < data[mid]:
				findK_func(data, left, mid-1, K)
			else if K > data[mid]:
				findK_func(data, mid+1, right, K)
		else:
			return False
	findK_func(data, 0, len(data)-1, K)


//题目2:在一个有序数组中,找>=某个数最左的位置.
返回的是位置的值。 二分到底,用一个变量记录最左侧的变量就是答案。
例如:数组为下, 找>=2的最左的位置(2)。
    1 1 2 2 2 2 2 2 2
	        √
	    √
	  ×   
def nearestIndex(data, K):
	index= 0 //记录最左的对号
	left = 0
	right = len(data)-1
	while right > left:
		mid = left + (right-left)//2
		if data[mid] >=K:
			index= mid
			right = mid - 1 // 达标后不要右边的,不达标则不要左边的,因为是有序数组
		else:
			left = mid + 1
	return index


//题目3:局部最小,整个数组无序,并且任意两个相邻的数字不相等。 只要返回一个局部最小就行。
'''
1. 两头判断局部最小
0位置 和 N-1 位置
0位置如果比1位置小,就是局部最小。
N-1位置如果比N-2位置小,就是局部最小。

2. 中间判断局部最小
中间位置i 要比i-1和i+1都小才是局部最小。

如果两头不是局部最小,则中间必有局部最小。
然后找中间位置,如果中间位置比两边都小 则返回中间值,如果不是 则返回比他小的一段 继续二分。
'''
def getLessIndex(data):
	if len(data) < 1:
		return -1
	if len(data) == 1 or data[0] < data[1]:
		return 0
	if data[len(data)-1] < data[len(data)-2]:
		return len(data)-1
	left = 1
	right = len(data) - 2
	mid = 0
	while left < right:
		mid = (right - left)//2 + left
		if data[mid] > data[mid-1]:
			right = mid - 1
		else if data[mid] > data[mid +1];
			left = mid + 1
		elsereturn mid
	return left

  1. 异或运算
    异或运算就是无进位相加。
    0+0 = 0 1+1 = 0 0+1 =1
    0^N == N
    N^N == 0
    异或满足交换律和结合律。 只要这些数不变,异或结果也不变,可以用无进位相加 来理解,i位置有偶数个1 结果为0,奇数个1结果为1.

    不用额外变量 交换a 和 b。 必须保证a 和b是内存里面两块东西,就可以这样做, 值可以一样,但是必须是不同的内存空间,即不同的东西。 如果a和b是同一个位置 比如是 数组里面的 i==j a=A[i] b=A[j] 这样就错了。
    两个不同的内存空间 则可以做下面操作;
    a = 甲, b= 乙
    方法: a = a^b --> a = 甲 ^ 乙
    b = a^b --> b = 甲 ^ 乙 ^ 乙 = 甲
    a = a^b --> a = 甲 ^ 乙 ^ 甲 = 乙

    一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到这个数。 —> 所有的数全部异或起来。

    一个数组中有两种数出现了奇数词,其他数都出现了偶数次,怎么找到这2个数。—> 设这两个数为a和b, 第一步: 所有数 异或 结果为 eor = a^b, 第二步: eor != 0 第i位数不为0,则 eor’ 异或所有的第i为0,则进行异或,

    提取一个数最右侧1. 取反加1 再和自己与。
    
  2. 对数器

    1. 有一个你想要的测的方法a.(比如插入排序)
    2. 实现复杂度不好但是容易实现的方法b (比如系统排序)
    3. 生成随机数据(大小和样本值),用方法a 测试一下,再用方法b测试一下,得出的结果一样就对了。
  3. 递归行为估计时间复杂度

    任何递归方法都可以改成非递归方法。

    递归方法: base case: 问题小到什么情况 就可以返回。 不是base case: 递归。

    递归是通过 系统栈 来实现的, 系统栈会把递归中所有的递归过程的现场信息。 一步步压栈,遇到base case后 返回,并且弹出栈中的东西 还原现场,继续往下跑。

    递归如何改成非递归? 不用系统栈,我们自己code实现压栈和出栈就实现非递归转换。

    递归函数的事件复杂度: T(N) = a * T(N/b) + O(N^d)
    只能估计子问题为相同规模的 递归。
    a 是递归有多少个子问题
    N/b 是子问题的规模

    1. log(b,a) > d : 复杂度为O(N^ log(b,a))
    2. log(b,a) = d : 复杂度为O(N^d * logN)
      3)log(b,a) < d:复杂度为O(N^d)
发布了28 篇原创文章 · 获赞 5 · 访问量 4325

猜你喜欢

转载自blog.csdn.net/m0_37531129/article/details/104100799