面试题——算法与数据结构Python实现

快速排序

参考:快速排序partition过程常见的两种写法+快速排序非递归实现

import random
def partition(arr, low, high):
    r=random.randint(low,high) # 随机选[low,high]内的数与arr[low]交换
    arr[r],arr[low]=arr[low],arr[r]
    pivot=arr[low]
    while low < high:
        while low < high and arr[high] >= pivot:
        	high-=1
        arr[low] = arr[high]#从后面开始找到第一个小于pivot的元素,放到low位置
        while low < high and arr[low] <= pivot:
        	low+=1
        arr[high] = arr[low]#从前面开始找到第一个大于pivot的元素,放到high位置
    arr[low] = pivot#最后枢纽元放到low的位置
    return low

def quick_sort(arr, low, high):
    if low < high:
        mid = partition(arr, low, high)
        quick_sort(arr, low, mid-1)
        quick_sort(arr, mid+1, high)

归并排序

参考:python归并排序–递归实现

def merge(left, right):
    """合并两个已排序好的列表,产生一个新的已排序好的列表"""
    result = []  # 新的已排序好的列表
    i = 0  # 下标
    j = 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    result += left[i:]
    result += right[j:]
    return result

def merge_sort(seq):
    if len(seq) <= 1:
        return seq
    mid = len(seq) // 2  # 将列表分成更小的两个列表
    # 分别对左右两个列表进行处理,分别返回两个排序好的列表
    left = merge_sort(seq[:mid])
    right = merge_sort(seq[mid:])
    # 对排序好的两个列表合并,产生一个新的排序好的列表
    return merge(left, right)

冒泡排序

def bubble_sort(arr):
    n = len(arr)
    # 进行n次冒泡,每次排好一个
    for i in range(n):
        # 最后 i 个已经有序
        for j in range(0, n-i-1):
            if arr[j] > arr[j+1] :
                arr[j], arr[j+1] = arr[j+1], arr[j]

二分查找

def binary_search(array,t):
    low = 0
    high = len(array)-1
    while low <= high:
        mid = (low+high)//2
        if array[mid] < t:
            low = mid + 1
       elif array[mid] > t:
            high = mid - 1
        else:
            return mid
    return -1

二分查找的拓展——上下界与个数问题

1、二分查找总结
2、二分查找算法及其扩展
3、二分查找的坑点与总结

注意点:

1、(low+high)/2 容易溢出
2、注意边界条件即各个while中的不等号处理

最小生成树

TODO

线段树和树状数组的异同

TODO

两个栈模拟队列和双端队列

栈 s1, s2 实现队列
()入队操作:
	将元素压入s1

()出队操作:
	if s2不为空:
		弹出栈顶元素
	elif s1不为空:
		将s1的元素逐个倒入s2,但最后一个元素弹出后直接返回而不用压入s2
	else:
		抛出队列为空的异常

双端队列:

栈 s1, s2 实现双端队列
后入队操作:
	将元素压入s1
前入队操作:
	将元素压入s2

前出队操作:
	if s2不为空:
		弹出栈顶元素
	elif s1不为空:
		将s1的元素逐个倒入s2,但最后一个元素弹出后直接返回而不用压入s2
	else:
		抛出队列为空的异常
后出队操作:
	if s1不为空:
		弹出栈顶元素
	elif s2不为空:
		将s2的元素逐个倒入s1,但最后一个元素弹出后直接返回而不用压入s1
	else:
		抛出队列为空的异常

最长上升子序列,dp实现,nlogn实现

TODO

快排时间复杂度和空间复杂度

最好情况是选中的pivot为中值,因此划分均衡

最坏情况是选中的pivot为最小值或最大值,导致每次划分为1个和 剩余

若选取pivot的方法是直接用low或high位置的元素,则最坏情况=数组有序,避免方法是随机选取pivot

时间复杂度

平均情况下是 O ( n log n ) O(n\log n) ,最坏情况下(数组有序)是 O ( n 2 ) O(n^2)

空间复杂度

快排的实现是递归调用的, 而且每次函数调用中只使用了常数的空间,因此空间复杂度等于递归深度,在和平均情况下为 O ( log n ) O(\log n) ,为 O ( n ) O(n)

布隆过滤器知道吗?用在什么场景下?推导会么(加分项)

详解布隆过滤器的原理,使用场景和注意事项

布隆过滤器-维基百科

K个链表归并

题目:https://leetcode.com/problems/merge-k-sorted-lists/submissions/

参考:https://leetcode.com/problems/merge-k-sorted-lists/discuss/10511/10-line-python-solution-with-priority-queue

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None

import queue
class Solution(object):
    def mergeKLists(self, lists):
        # 我们放入队列的元素是(node.val,node),如果有相同val的node,就会调用node的比较方法
        ListNode.__lt__ = lambda x, y: True if x.val < y.val else False 
        dummy = ListNode(None)
        curr = dummy
        q = queue.PriorityQueue()
        for node in lists:
            if node is not None: 
                q.put((node.val,node))
        while q.qsize()>0:
            curr.next = q.get()[1]
            curr=curr.next
            if curr.next is not None: 
                q.put((curr.next.val, curr.next))
        return dummy.next

二叉树前序中序遍历,重建二叉树

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        if inorder:
            ind = inorder.index(preorder.pop(0))
            root = TreeNode(inorder[ind])
            root.left = self.buildTree(preorder, inorder[0:ind])
            root.right = self.buildTree(preorder, inorder[ind+1:])
            return root

一般的实现是将preorder划分两部分给左右子树的构建,但是这里用pop()弹出了结点,而且是先对左子树构建,所以到了右子树的时候,preorder中左子树的所有结点刚好pop完了

注意:若给前序和中序,直接问后序遍历的结果,可以先建树,然后再后序遍历。

二叉树的前序遍历和中序遍历的非递归实现

参考:https://blog.csdn.net/Applying/article/details/84982712

前序遍历

# 二叉树结点定义见前面的《二叉树前序中序遍历,重建二叉树 》
class Solution:
    def preorderTraversal(self, root: TreeNode) -> List[int]:
        res = []
        if root is None:
            return res
        stack = []
        stack.append(root)
        while len(stack)!=0:
            p = stack.pop()
            # 访问,即加入结果list
            res.append(p.val)
            # 这里注意,要先压入右子节点,再压入左节点
            if p.right is not None:
                stack.append(p.right)
            if p.left is not None:
                stack.append(p.left)
        return res
        
# 二叉树结点定义见前面的《二叉树前序中序遍历,重建二叉树 》
class Solution:
    def inorderTraversal(self, root: TreeNode) -> List[int]:
        res=[]
        if root is None:
            return res
        
        stack = []
        p = root
        # 栈非空 或 p非空
        while len(stack)!=0 or p is not None:
            # 向左搜索,寻找最左的节点,即中序遍历的第一个节点
            while p is not None:
                stack.append(p)
                p = p.left
            # 对每一个节点进行判断
            if len(stack)!=0:
                # 访问当前节点
                r = stack.pop()
                res.append(r.val)
                # 遍历其右子树
                p = r.right
        return res

哈希表相关

解决哈希冲突:

  • 开放定址法
    • 线性探测法
    • 平方探测法
  • 链地址法

常用哈希函数:课本常介绍除留余数法,Fasttext用的是FNV-1a,文件校验常用MD5、SHA-1

大数相加,大数相乘

TODO

判断完全二叉树、满二叉树、二叉搜索树BST

判断二叉树是否为完全二叉树

以层次遍历的方法, 找到第一个两个儿子不都存在的节点
if 此节点没有左子树且有右子树:
	不是完全二叉树
else 看下一个节点:
	如果下一个节点是叶子,则是完全二叉树,否则不是完全二叉树

判断是否为满二叉树

# 返回是否为满二叉树以及该树的最大深度
def is is_full(p):
	if p is None:
		return True, 0
	else:
		lres, lhei = is_full(p.left)
		rres, rhei = is_full(p.right)
		if lres and rres and lhei==rhei: # 左子树和右子树都是满二叉树且高度相等
			return True, lhei+1
		else:
			return False, max(lhei, rhei)+1

判断是否为BST

中序遍历的结果若有序则是BST,否则不是

链表反转

头插法

反转二叉树(镜像二叉树)

交换树中所有节点的左右子节点,即得到树的镜像。

def invert(root):
	if root is None:
		return root
	tmp = root.left
	root.left = invert(root.right)
	root.right= invert(tmp)
	return root

稳定和非稳定的排序算法有哪些

稳定算法包括:归并、冒泡、插入、基数。“归泡插基”

在这里插入图片描述

二分查找递归和非递归的时间和空间复杂度

非递归:时间 O ( log n ) O(\log n) ,空间 O ( 1 ) O(1)

递归:时间 O ( log n ) O(\log n) ,空间 O ( log n ) O(\log n)

输入补全可以用哪个数据结构来做

字典树,见:如何实现搜索框的关键词提示功能

编辑距离

LeetCode动态规划题目总结

和等于 k 的最短子数组长度

要求:如果用动态规划要优化时间,用贪心法需要证明

Leetcode有道类似的:

和等于 k 的最长子数组长度

判断是否为对称二叉树

def sym(p, q):
    if p is None and q is None:
        return True
    elif p is not None and q is not None:
        # 对称的条件:p和q的值相同,并且p的左子树与q的右子树对称,p的右子树与q的左子树对称
        return (p.val == q.val) and sym(p.left,q.right) and sym(p.right,q.left)
    return False
    
class Solution:
    def isSymmetric(self, root: TreeNode) -> bool:
        return root is None or sym(root.left,root.right)

旋转数组:不用额外内存

def reverse(nums,low,high):
    while low < high:
        nums[low], nums[high] = nums[high], nums[low]
        low+=1
        high-=1

class Solution:
    # Do not return anything, modify nums in-place instead.
    def rotate(self, nums: List[int], k: int) -> None:   
        k %= len(nums)
        reverse(nums, 0, len(nums)-1)
        reverse(nums, 0, k - 1)
        reverse(nums, k, len(nums) - 1)

二叉树的序列化与反序列化

剑指offer:二叉树序列化与反序列化

第k大的数,两种做法 (quick select, heap)

在从小到大排序后的数组中,第 k k 大的数是从 1 1 开始算的第 n k + 1 n-k+1 小的数,下标从 0 0 开始,因此第 k k 大的数下标 n k n-k .

方法一:quick select

class Solution {
public:
    int partition(vector<int>& arr,int low,int high){
        int r = (rand() % (high-low+1))+ low; // [low, high]
        swap(arr[r], arr[low);
        int pivot = arr[low];      
        while(low<high){
            while(low<high && arr[high]>=pivot)high--;
            arr[low]=arr[high];
            while(low<high && arr[low]<=pivot)low++;
            arr[high]=arr[low];
        }
        arr[low]=pivot;
        return low;
    }
    int findKthLargest(vector<int>& nums, int k) {
        int mid=-1, low=0, high=nums.size()-1;
        while(true){
            mid = partition(nums,low,high);
            if(mid==nums.size()-k){
                return nums[mid];
            }else if(mid<nums.size()-k){
                low=mid+1;
            }else{
                high=mid-1;
            }
        }
    }
};

使用优先队列,用法见:c++优先队列(priority_queue)用法详解

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
    	// 默认是大顶堆,使用greater<T> 是小顶堆
        priority_queue<int,vector<int>,greater<int>> q; 
        for(auto it:nums){
            q.push(it);
            // pop掉最小的,剩下k个最大的,最后的top()就是k个最大中的最小值
            if(q.size()>k) q.pop();
        }
        return q.top();
    }
};

判断链表是否有环以及环的位置,证明

class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        if not head or not head.next: # 空链表或只有一个节点时没有环
            return False
        slow,fast=head,head
        while fast and fast.next:
            slow=slow.next
            fast=fast.next.next
            if slow==fast:
                break
        return slow==fast

证明寻找环的起点的算法:

http://www.bnee.net/article/20526.html

在这里插入图片描述

1、慢指针slow走到了环入口时,设共走了k步。此时快指针fast越过了环入口的步数为delta。因为快指针可能绕着环走了很多圈,所以有k = delta + n * R,其中R为环的大小,n为快指针绕环走的圈数。

2、证明必然会相遇。慢指针进入环中后,因为快指针每次都比慢指针快一步,所以,快慢指针最后一定会相遇。

在这里插入图片描述
3、计算快慢指针相遇位置。因为慢指针在刚进入环时距离快指针delta步,所以快指针还需要比慢指针多走R - delta步才能与慢指针相遇。又因为快指针每次走两步,所以快指针还需要走2(R - delta)步。那么,相对环的起点而言,相遇位置为2(R - delta) + delta = 2R - delta,即,距离环入口delta处,与慢指针刚进入环时快指针所在位置对称。

4、快指针重新从头结点开始走,速度为一次一步,与慢指针相同。可知,快指针走到环入口时,所需步数为k。由于 k = delta + n * R,所以慢指针也是刚好走到环入口。

class Solution:
    def detectCycle(self, head: ListNode) -> ListNode:
        if not head or not head.next:
            return None
        slow,fast=head,head
        while fast and fast.next:
            slow=slow.next
            fast=fast.next.next
            if slow==fast:
                break
        if slow==fast:
            fast=head
            while slow!=fast:
                slow=slow.next
                fast=fast.next
            return slow
        return None

青蛙跳台阶+有一次后退机会

面经看到,但是搜索不到这道题的解法,我目前的个人思路是这样的:

先当做没有后退机会来算,然后考虑一次后退机会加在不同位置带来的新的跳法总共多少种。

设有 n n 级台阶,分别标号 [ 1 , 2 , . . . , n ] [1,2,...,n] ,我们一开始处于位台阶 0 0 ,最后要到达位置 n n .

当没有后退机会时,设从台阶 0 0 到达台阶 i i 共有的跳法种数是 d p [ i ] dp[i] ,则 d p [ 0 ] = 0 , d p [ 1 ] = 1 , d p [ i ] = d p [ i 1 ] + d p [ i 2 ] , i 2 dp[0]=0,dp[1]=1,dp[i]=dp[i-1]+dp[i-2],其中i≥2

然后考虑一次后退机会加在不同位置带来的新的跳法总共多少种

如果后退之后到达的是台阶 i i ,那么产生新的跳法数是 “从台阶 i i 到台阶 n n 的跳法种数”,这应该等价于 “从台阶 0 0 到台阶 n i n-i 的跳法种数”,也就是 d p [ n i ] dp[n-i] .

由上可知,考虑上后退位置的不同,

= 退 + 退 = d p [ n ] + ( d p [ n ] + d p [ n 1 ] + + d p [ 1 ] ) n 退 n = d p [ n ] + i = 1 n d p [ i ] \begin{aligned} 总的跳法总数 &= 没有后退机会的种数+不同位置的一次后退带来的新增跳法种数\\ &=dp[n]+(dp[n]+dp[n-1]+\cdots +dp[1]) (不能超出第n级再后退到第 n级)\\ &=dp[n]+\sum_{i=1}^n dp[i] \end{aligned}

也就是按照没有后退机会求出前面所定义的 d p dp 数组后,对数组求和再加上 d p [ n ] dp[n] 即为所求,用python就是return dp[n] + sum(dp)

最长公共子串、最长公共子序列

LeetCode动态规划题目总结(持续更新中)

二叉树路径最大和

https://leetcode.com/problems/binary-tree-maximum-path-sum/

蓄水池采样问题

https://www.jianshu.com/p/7a9ea6ece2af

https://blog.csdn.net/anshuai_aw1/article/details/88750673

给定一个数据流,数据流长度N很大,且N直到处理完所有数据之前都不可知,请问如何在只遍历一遍数据(O(N))的情况下,能够随机选取出k个不重复的数据。

这个场景强调了3件事:

  • 数据流长度N很大且不可知,所以不能一次性存入内存。
  • 时间复杂度为O(N)。
  • 随机选取k个数,每个数被选中的概率为k/N。

思路:

1、前 k 个数据直接放入蓄水池。
2、蓄水池装满了 k 个数据之后,当接收到第 i 个数据时,在 [0, i] 范围内取随机数 r,若 r 落在[0, k-1]范围内,则用接收到的第i个数据替换蓄水池中的第 r 个数据。
3、重复步骤2。

核心代码如下:

int[] reservoir = new int[k];

// init
for (int i = 0; i < reservoir.length; i++){
    reservoir[i] = dataStream[i];
}

for (int i = k; i < dataStream.length; i++){
    // 随机获得一个[0, i]内的随机整数
    int r = rand.nextInt(i + 1);
    // 如果随机整数落在[0, m-1]范围内,则替换蓄水池中的元素
    if (r < k){
        reservoir[r] = dataStream[i];
    }
}

:这里使用已知长度的数组dataStream来表示未知长度的数据流,并假设数据流长度大于蓄水池容量m。

证明:

先求第 i i 个数 ( i k ) (i≤k) 在第 N N 步之后被保留在蓄水池的概率 P 1 P_1 。在第 k + 1 k+1 步时,它被保留的概率是 1 1 k + 1 = k k + 1 1-\frac{1}{k+1}=\frac k{k+1} ,在第 k + 2 k+2 步时,被保留的概率是 1 1 k + 2 = k + 1 k + 2 1-\frac{1}{k+2}=\frac {k+1}{k+2} ,以此类推,可知:

P 1 = k = k k + 1 × k + 1 k + 2 × × n 2 n 1 × n 1 n = k n \begin{aligned} P_1 &= 第 k 步后的每一步都被保留在蓄水池的概率的乘积\\ &=\frac k{k+1} \times \frac {k+1}{k+2} \times \cdots\times \frac {n-2}{n-1} \times \frac {n-1}{n}\\ &=\frac{k}{n} \end{aligned}

再求第 j j 个数 ( j > k ) (j>k) 在第 N N 步之后被保留在蓄水池的概率 P 2 P_2 。最后要保留必须在遇到它时将该数用于替换蓄水池中的数据,而且之后的每一步他都没有被替换掉。在第 j j 步时,该数被保留下来的概率是 k j \frac k{j} ,在第 j + 1 j+1 步时,被保留在蓄水池的概率是 1 1 j + 1 = j j + 1 1-\frac{1}{j+1}=\frac{j}{j+1} ,以此类推,可知:

P 2 = j = k j × j j + 1 × n 2 n 1 × n 1 n = k n \begin{aligned} P_2 &= 第 j 步被保留的概率乘以后面每一步都被保留的概率\\ &=\frac k{j} \times \frac{j}{j+1}\cdots\times \frac {n-2}{n-1} \times \frac {n-1}{n}\\ &=\frac{k}{n} \end{aligned}

综上可知,每个数在第 N N 步之后被保留的概率都是 k n \frac{k}{n}

2 sum,3 sum

【算法】2SUM/3SUM/4SUM问题

一个排序数组能够构成多少个二叉搜索树

https://leetcode.com/problems/unique-binary-search-trees/

LeetCode动态规划题目总结(持续更新中)

分解质因数

def get_num_factors(num):
    res = []
    tmp = 2
    if num <= 3:
        return [num]
    while num >= tmp:
        if num % tmp == 0:
            res.append(tmp)
            num = num // tmp  # 更新
        else:
            tmp = tmp + 1  # 同时更新除数值,不必每次都从头开始
    return res
发布了67 篇原创文章 · 获赞 27 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/xpy870663266/article/details/104643841