本课程是牛客网 左程云的课程总结
-
时间复杂度
常数时间的操作: 一个操作是固定时间的,与样本量没有关系,每次都是固定时间内完成的操作。
比如 数组的寻址操作(通过偏移量来获取),位运算(左移,右移,或,异或等)
非常数时间操作: 比如 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)
'''
-
选择排序
时间复杂度 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]
- 冒泡排序
时间复杂度 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]
-
插入排序
时间复杂度是 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: 在一个有序数组中,找某个数是否存在
题目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
else:
return mid
return left
-
异或运算
异或运算就是无进位相加。
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 再和自己与。
-
对数器
- 有一个你想要的测的方法a.(比如插入排序)
- 实现复杂度不好但是容易实现的方法b (比如系统排序)
- 生成随机数据(大小和样本值),用方法a 测试一下,再用方法b测试一下,得出的结果一样就对了。
-
递归行为估计时间复杂度
任何递归方法都可以改成非递归方法。
递归方法: base case: 问题小到什么情况 就可以返回。 不是base case: 递归。
递归是通过 系统栈 来实现的, 系统栈会把递归中所有的递归过程的现场信息。 一步步压栈,遇到base case后 返回,并且弹出栈中的东西 还原现场,继续往下跑。
递归如何改成非递归? 不用系统栈,我们自己code实现压栈和出栈就实现非递归转换。
递归函数的事件复杂度: T(N) = a * T(N/b) + O(N^d)
只能估计子问题为相同规模的 递归。
a 是递归有多少个子问题
N/b 是子问题的规模- log(b,a) > d : 复杂度为O(N^ log(b,a))
- log(b,a) = d : 复杂度为O(N^d * logN)
3)log(b,a) < d:复杂度为O(N^d)