复杂度
常见的时间复杂度
- O(1):
执行常数次,和输入无关
def O1(num):
i = num
j = num*2
return i+j
- O(N):
def ON(num):
total = 0
for i in range(num):
total+=i
return total
- O(logN):
def OlogN(num);
i = 1
while(i < num):
i = i*2
return i
- O(M+N)
def OMN(num):
total = 0
for i in range(num):
total += 1
for j in range(num):
total += j
return total
- O(NlogN)
def ONlogN(num1, num2):
total = 0
j = 0
for i in range(num1):
while(j < num2):
total += i + j
j = j*2
return total
- O(N^2)
def ON2(num):
total = 0
for i in range(num):
for j in range(num):
total += i + j
return total
O(1) < O(logN) (二分查找) < O(N) < O(NlogN) < O(N^2) < O(2^n) < O(n!)
常见的空间复杂度
算法的存储空间与输入值之间的关系
O(1) < O(N) < O(N^2)
常量看其与输入值得关系
递归要考虑递归栈
O(1)
def ON(num):
sum = 0;
for i in range(num):
sum = sum+i
return sum
递归
O(1)
def ON(num):
if(num<=0):
return 0
return ON(num-1) + ON(num-2)
数据结构
数组
- 定义:在连续的内存空间中,储存一组相同类型的元素
- 数组的访问: 通过索引访问元素。a[0]
- 数组的内存空间是连续的,增删需要移动后面的元素
- 二维数组的内存地址是连续的吗?
二维数组实际是一个线性数组存放着其他数组的首地址
- 数组的搜索:找到这个元素对应的索引
复杂度:
- 访问Access:O(1)
通过计算可以得到地址位置,从而进行访问 - 搜索search:O(N)
需要对数组进行遍历 - 插入insert: O(N)
需要将后面的元素往后移动
如果内存不够,需要开辟一块新空间,将数组移进去 - 删除delete: O(N)
需要将后面元素往前移
特点
- 适合读
- 不适合频繁做增删操作。
- 场景:读多写少
常用操作
- 创建数组
a = [];
- 添加元素
# 末尾添加
a.append(1)
a.append(2)
a.append(3)
# [1,2,3]
# insert(x,y)指定位置x添加y
a.insert(2,99)
# [1,2,99,3]
- 访问元素
temp = a[2]
# temp = 99
- 修改元素
a[2] = 88
- 删除元素
# 删除指定值 O(N)
a.remove(88)
# 1 2 3
# 删除指定索引的元素 O(N)
a.pop(1)
# 1 2
# 删除最后的元素 O(1)
a.pop()
# 1
- 遍历元素
for i in a:
print(i)
#
for i in range(0,len(a)):
print("index:",i,"value:",a[i])
#
for i, val in emumerate(a):
print("index:",i,"value:",val)
- 查找元素
index = a.index(2)
- 数组的长度
len(a)
- 数组的排序
# 从小到大
a.sort()
# 从大到小
a.sort(reverse =True)
练习题
class Solution:
def findMaxConsecutiveOnes(self, nums: List[int]) -> int:
count = 0
myMax = 0
for i in nums:
if(i == 1):
count =count+1
myMax = max(myMax,count)
else:
count = 0
return myMax
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
j = 0;
for i in range(0,len(nums)):
if(nums[i] !=0):
nums[j] = nums[i]
j+=1
else:
continue
for k in range(j,len(nums)):
nums[k] = 0
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
count = 0;
j = 0
for i in range(0,len(nums)):
if(nums[i] != val):
nums[j] = nums[i]
j+=1
else:
continue
return j
链表
- 非连续空间,包含当前数据和下一节点的地址
复杂度
- 访问access O(N)
- 搜索 O(N)
- 插入 O(1)
- 删除 O(1)
场景
读少写多
常用操作
- 创建链表
linkedldist = deque()
- 添加元素
# 尾部添加
linkedlist.append(1)
linkedlist.append(2)
linkedlist.append(3)
# 指定位置添加 O(n)
linkedlist.insert(2,99)
- 访问元素
# O(N)
element = linkedlist[2]
# 99
- 查找元素
O(N)
index = linkedlist.index(99)
# 2
- 删除元素
O(N)
# 删除元素
linkedlist.remove(1)
# 删除指定索引的元素
del linkedlist[2]
- 更新元素
O(N)
linkedlist[2] = 88
- 链表的长度
O(1)
length = len(linkedlist)
练习题
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def removeElements(self, head: ListNode, val: int) -> ListNode:
dummy = ListNode(0)
dummy.next = head
temp = dummy
while(temp.next):
if(temp.next.val ==val):
temp.next = temp.next.next
else:
temp = temp.next
return dummy.next
队列
- 先进先出
- 基于链表创建的
单端队列: 一个口进一个口出
双端队列: 两个口都可以进,两个口都可以出
复杂度
- 访问: O(N)
- 搜索:O(N)
- 插入: O(1)
- 删除: O(1)
常用操作
python
- 创建队列
# 创建队列
# 该方法为双端队列
queue = deque()
- 添加元素
# 向末尾添加
queue.append(1)
# 向队列头添加
queue.appendleft(1)
- 获取即将出队的元素
# 获取即将出队的元素 O(1)
temp1 = queue[0]
# 获取队尾元素 O(1)
temp2 = queue[-1]
temp2 = queue[len(queue)-1]
- 删除即将出队的元素
# 删除即将出队的元素 O(1)
# 会返回元素
temp2 = queue.popleft()
# 删除最右元素
queue.pop()
- 判断队列是否为空
# deque每次添加删除元素时会自动计数 O(1)
len(queue) == 0
- 队列长度
# deque每次添加删除元素时会自动计数 O(1)
len(queue)
- 遍历队列
# 变删除边遍历
while len(queue) !=0:
temp = queue.popleft()
print(temp)
C++
- 头文件
#include <queue>
- 创建队列
// 创建队列
// 该方法为双端队列
queue<string> que;
- 添加元素
# 向末尾添加
que.push("kobe");
- 删除队列首元素但不返回其值
que.pop();
- 返回队首元素的值,但不删除该元素
que.front();
- 返回队列尾元素的值,但不删除该元素
que.back()
- 判断队列是否为空
que.empty()
- 队列长度
que.size()
练习题
Leetcode 239
经典单调队列题目
思路:
将滑动窗可以看成一个双端队列。双端队列的最大值放在头部,当头部与滑动窗口出窗的元素相同时,则弹出队头。滑动窗要新进入的元素与队列的尾部元素进行比较,如果比尾部元素大,这弹出尾部元素,因为尾部元素肯定不可能为最大元素。直到尾部元素比要进队元素大或者队列为空时,将进队元素压入队尾。
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
queue = deque()
res = []
for i in range(k):
while (len(queue)!=0 and nums[i] > queue[len(queue)-1]):
queue.pop()
queue.append(nums[i])
res.append(queue[0])
for i in range(k,len(nums)):
if(queue[0]==nums[i-k]):
queue.popleft()
while (len(queue)!=0 and nums[i] > queue[len(queue)-1]):
queue.pop()
queue.append(nums[i])
res.append(queue[0])
return res
Stack
- 先进后出
- 基于链表创建的
单端队列: 一个口进一个口出
双端队列: 两个口都可以进,两个口都可以出
复杂度
- 访问: O(N)
- 搜索:O(N)
- 插入: O(1)
- 删除: O(1)
python
常用操作
- 创建stack
# 创建stack
stack = []
- 添加元素
# 向末尾添加
stack.append(1)
- 获取即将出stack的元素
# 获取即将stack的元素 O(1)
temp1 = stack[-1]
- 删除即将stack的元素
# 删除即将出stack的元素 O(1)
# 会返回元素
temp = stack.pop()
- 判断队列是否为空
# stack每次添加删除元素时会自动计数 O(1)
len(stack) == 0
- stack长度
# stack每次添加删除元素时会自动计数 O(1)
len(stack)
- 遍历stack
# 变删除边遍历
while len(stack) !=0:
temp = queue.pop()
print(temp)
C++
练习题
Hash
- Key - Hash Function - Address
- Key - Value
哈希碰撞:两个不同的key通过同一个hash函数得到相同的内存地址
4通过哈希函数解析出的地址也是1,冲突了。
解决方法:
- 链表法, 在后面通过链表加入4.bish
复杂度
- 访问: 没有这个方法
- 搜索:O(1), 如果有hash碰撞的情况下,就不是O(1)了,为O(K), K为碰撞元素的个数
- 插入: O(1)
- 删除: O(1)
常用操作
Python
- 创建哈希表
# 使用数组创建hash
hashTable = ['']x4
# 使用字典创建hash
mapping = {}
- 添加元素
# 数组添加
hashTable[1] = 'hanmeimei'
hashTable[2] = 'lihua'
hashTable[3] = '233'
mapping[1] = 'hanmeimei'
mapping[2] = 'lihua'
mapping[3] = '233'
- 修改元素
hashTable[1] = 'unspoken'
mapping[1] = 'erd'
- 删除
hashTable[3] = ''
mapping.pop(1) # 删除key=1的值
# 或者使用del
del mapping[1]
- 获取Key对应的值
hashTable[3]
mapping[2]
- Key是否存在
#数组类型的只能遍历
# 返回true或false
3 in mapping
- hash长度
len(mapping)
# empty?
len(mapping)==0
C++
map可以分为4类:
map: 按顺序保存,key对应 唯一 value的map
multimap; 按 顺序 保存,key可对应 多个 value的map
unordered_map: 无序 保存,key对应 唯一 value的map
unordered_multmap: 无序 保存,key对应 多个 value的map
- 头文件
#include <map>
- 创建哈希表
// 未初始化
map<int, string> hashTable;
// 初始化
map<int, string> hashTable = {
{ 2015, "Jim" },
{ 2016, "Tom" },
{ 2017, "Bob" } };
- 添加元素
// 使用[ ]进行单个插入
hashTable[2018] = "kobe";
// 使用insert插入
// 插入单个值
hashTable.insert(std::pair<int, std::string>(2019,"lebron"));
// 指定位置插入
std::map<int, std::string>::iterator it = hashTable.begin();
hashTable.insert(it, std::pair<int, std::string>(2011,"ivson"));
// 列表形式插入
hashTable.insert({{2012,"end"},{2033,"punk"}});
- 修改元素
hashTable[2018] = 'unspoken'
- 删除
// 删除迭代器指定位置,并返回一个指向下一元素的迭代器
hashTable.erase(hashTable.begin());
// 删除一定范围内的元素,并返回一个指向下一元素的迭代器
// map的迭代器是双向迭代器,只能it++, it--, ++it, --it操作,不能,it+5这样操作
// http://c.biancheng.net/view/338.html
// 删除[a, b]的所有元素
auto it = hashTable.begin();
it++;
hashTable.erase(hashTable.begin(), it);
// 根据Key来进行删除, 返回删除的元素数量,在map里结果非0即1
size_t num = hashTable.erase(2012);
// 清空map,清空后的size为0
hashTable.clear();
- 获取Key对应的值
Map中元素取值主要有at和[ ]两种操作,at会作下标检查,而[]不会。
// key 2000不在hashtable中,使用[]不会报错,会插入该key,但不会显示内容
std::cout<<hashTable[2000] <<std::endl;
// at会报错
std::cout << hashTable.at(2000) <<std::endl;
- Key是否存在
// 使用find进行查找,找到则返回指向该关键字的迭代器,否则返回指向end的迭代器
if(hashTable.find(2011)!=hashTable.end()){
std::cout << "find it" <<std::endl;
}
- hash长度
hashTable.size();
# empty?
hashTable.empty();
- 排序
// map不支持c++内置的sort,所以使用vector<pair<int, string>> 当做伪map,然后再用sort处理
vector<pair<int, string>> mymap = {{1000,"M"},{900,"CM"},{500,"D"},{400,"CD"},{100,"C"},{90,"XC"},{50,"L"},{40,"XL"},{10,"X"},{9,"IX"},{5,"V"},{4,"IV"},{1,"I"}};
// 将其从小到大排序
sort(mymap.begin(),mymap.end(),[](const pair<int, string>& a, const pair<int,string>& b){
return a.first <= b.first;
});
练习题
思路:
将数组中的数看做key,出现相同的key,则在key对应的value上+1
然后判断,如果有value>1的情况,则说明有重复元素,返回true
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
mapping = {}
for i in nums:
if i not in mapping:
mapping[i] = 1
else:
mapping[i] = mapping[i]+1
if mapping[i] > 1:
return True
return False
思路:
将字符串中的元素看做key,出现相同的key,则在key对应的value上+1
然后对t字符串的元素进行遍历,如果该元素没有出现在mapping中,或者该元素对应的value为0,则返回该元素
class Solution:
def findTheDifference(self, s: str, t: str) -> str:
mapping = {}
for i in s:
if i not in mapping:
mapping[i] = 1
else:
mapping[i]+=1
for i in t:
if i not in mapping or mapping[i] is 0:
return i
else:
mapping[i]-=1
return "a"
思路:
单调栈的应用:
场景:寻找下一更大元素(单调递减栈),或更小元素(单调递增栈);
使栈里的元素呈现单调递增或者递减
以此题为例,如果想要让栈里元素递减,则当要入栈元素>栈顶元素,则弹出栈顶元素,并用哈希表保存栈顶和入栈元素。直至出现比入栈元素大的栈顶元素,或者栈空。
遍历完元素后,栈中剩余元素,规定其对应的值为-1,存入hash表中
class Solution:
def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:
stack = []
mapping = {}
for i in nums2:
if len(stack) == 0:
stack.append(i)
else:
while len(stack) > 0 and stack[-1] < i:
t1 = stack[-1]
mapping[t1] = i
stack.pop()
stack.append(i)
while len(stack) != 0:
mapping[stack[-1]] = -1
stack.pop()
res = []
for i in nums1:
res.append(mapping[i])
return res
Set
- 无序
- 不重合
主要作用:检查某一个元素是否存在
有没有重复元素
复杂度
- 访问: 没有这个方法
- 搜索:O(1), 如果有hash碰撞的情况下,就不是O(1)了,为O(K), K为碰撞元素的个数
- 插入: O(1); 有hash冲突O(k)
- 删除: O(1); 有hash冲突O(k)
常用操作
- 创建Set
# 使用数组创建hash
s=set()
- 添加元素
s.add(18)
s.add(10)
s.add(1)
- 删除
s.remove(2)
- 元素是否存在
# 返回true或false
3 in s
- hash长度
len(s)
# empty?
len(s)==0
C++
set可以分为4类:
set: 按顺序保存,元素只能出现1次
multiset; 按 顺序 保存,元素只能出现多次
unordered_set: 无序 保存,元素只能出现1次
unordered_multset: 无序 保存,元素只能出现多次
树
- 节点:除了根节点和叶子节点外的节点
- 根节点:第一个开始的节点
- 叶子节点:最底层的节点,没有孩子、
深度:从上往下计算
高度:从下往上计算
层:从上往下计算。1开始
遍历:
二叉树d的遍历
- 前序遍历:先访问根节点,然后访问左节点,最后右节点
- 中序遍历:先访问左节点,然后访问根节点,最后右节点
- 后续遍历:先访问左节点,然后访问右节点,最后根节点
前缀树
- 匹配与前缀相同的字符串
复杂度
插入: O(N)
搜索:O(N)
前缀(prefix): O(N)
堆(heap)
- 完全二叉树
- 每个节点>=(最大堆) or <=(最小堆)孩子节点
复杂度
访问(acess):无
搜索:O(1) (堆顶)
添加:O(logN)
删除:O(logN) 一般是堆顶
Leetcode 208, 720, 692
复常用操作
- 创建堆
# 使用数组创建heap
minheap = []
heapq.heapify(minheap)
- 添加元素
heapq.heappush(minheap,10)
heapq.heappush(minheap,8)
heapq.heappush(minheap,6)
heapq.heappush(minheap,2)
heapq.heappush(minheap,11)
- 查看堆顶元素
# peek
print(minheap[0])
- 删除
heapq.heappop(minheap)
- 长度
len(minheap)
# empty?
len(s)==0
- Iteration
while len(minheap)!=0:
print(heap.heappop(minheap))
如何获取最大堆
将元素取反,然后对其进行最小堆处理,即是最大堆
如1,3,6,8
取反-1,-3,-6,-8
最小堆的堆顶-8,-(-8) = 8即为最大堆的堆顶
图
-
无向图
-
有向图
-
权重图
-
入度:多少边指向该顶点
-
出度:多少边从这个点指向别的顶点
数据结构知识点回顾
各种数据结构:
- 访问
- 搜索
- 插入
- 删除
各种数据结构相对应的时间复杂度
算法部分
二分查找
适用场景
一般适用场景是有序数组中查找指定元素。
模板
- 目标值包含在我们设置的范围中(通过mid来判定)、
# Input: vector<int> nums, targer
int left = 0;
int right = nums.size()-1;
while(left <=right){
# 为了防止溢出
int mid = left + (right - left) /2;
if(nums[mid] == target){
return mid;
}else if(nums[mid] < target){
left = mid+1;
}else{
right = mid - 1;
}
}
- 每次迭代都是为了排除非目标数。朝着目标数逐渐逼近。最后的left有可能是target。有时候还需要对跳出循环的left进行判断处理。
# Input: vector<int> nums, targer
int left = 0;
int right = nums.size()-1;
while(left < right){
int mid = left + (right - left)/2;
if(nums[mid] < target){
left = mid+1;
}else{
right = mid;
}
}
if(nums[left] == target){
return left;
}else{
return -1;
}
注意事项
在写二分法题目时,一定要注意各自变量代表的含义,特别是区间的开闭情况。
一般我喜欢写区间,前后都为闭区间[left,right]
while的跳出条件
left <right; 这时循环中不能判定left==right的情况,即无法判定最后的mid
left<=rigth;这时循环中可以判定left==rigth的情况。循环中应该判定nums[mid] == target.
切记二分法不能生搬硬套。一定要理解,灵活变通,特别是边界条件的判断。
特殊情况
异或问题
- 运算方法
int a = 12;
int b = 16;
// ^异或位运算符
cout << a^b << endl;
异或问题的题目常常会使用异或运算的下列性质:
对于特定数组 [a1, a2, a3, … , an][a1,a2,a3,…,an],要求得任意区间 [l, r][l,r] 的异或结果,可以通过 [1, r][1,r] 和 [1, l - 1][1,l−1] 的异或结果得出:
xor(l,r)=xor(1,r)⊕xor(1,l−1)
因为:
xor(1,l-1)⊕xor(1,l-1) = 0
xor(1,r) = xor(1,l-1)⊕xor(l,r)
所以一般可以先遍历数组将异或结果存入数组。
位运算
符号 | 描述 | 运算规则 |
---|---|---|
& | 与 | 两个位都为1时,结果才为1 |
| | 或 | 两个位都为0时,结果才为0 |
^ | 异或 | 两个位相同为0,相异为1 |
~ | 取反 | 0变1,1变0 |
<< | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>> | 右移 | 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) |
左移,右移时注意是否有符号
无符号类型: 最高位仍然表示数值
有符号类型: 最高位表示符号,1为负,0为正
左移: << ; 无符号数–> 最右补零;有符号数: 最右补零,最高位可能变化,正负号可能改变。
右移: >> ; 右符号数–>左边补0;有符号数: 左边补0或1,保持符号位。
注意优先级,添加括号
char类型的运算对象首先提升成int型,提升时原来位保持不变,高位补零。
- 获得int型最大值
int getMaxInt(){
return (1 << 31) - 1;//2147483647, 由于优先级关系,括号不可省略
}
- 乘以2的m次方
int mulTwo(int n,int m){//计算n*2
return n << m;
}
- 除以2的m次方
int divTwo(int n,int m){//负奇数的运算不可用
return n >> m;
}
- 判断一个数的奇偶性
boolean isOddNumber(int n){
return (n & 1) == 1;
}
- 从低位到高位,取n的第m位
int getBit(int n, int m){
return (n >> (m-1)) & 1;
}
- 从低位到高位.将n的第m位置1
int setBitToOne(int n, int m){
return n | (1 << (m-1));
/*将1左移m-1位找到第m位,得到000...1...000
n在和这个数做或运算*/
}
- 从低位到高位,将n的第m位置0
int setBitToZero(int n, int m){
return n & ~(1 << (m-1));
/* 将1左移m-1位找到第m位,取反后变成111...0...1111
n再和这个数做与运算*/
}