用Python/Java手把手实现面试高频算法:从快排、LRU到K-Means,附完整代码与复杂度分析

张开发
2026/6/13 3:16:59 15 分钟阅读
用Python/Java手把手实现面试高频算法:从快排、LRU到K-Means,附完整代码与复杂度分析
从理论到实践三大经典算法的手把手实现与优化指南在计算机科学的面试中算法实现能力往往是区分优秀候选人的关键指标。无论是快速排序这样的基础算法还是LRU缓存淘汰这样的系统设计难题亦或是K-Means这样的机器学习核心方法能够清晰理解原理并熟练编写代码的开发者总能脱颖而出。本文将带您深入这三种算法的实现细节不仅提供可运行的Python/Java代码还会分析各种边界条件的处理方法最后从时间复杂度和空间复杂度两个维度评估算法性能。1. 快速排序分而治之的艺术快速排序是面试中最常被要求手写的算法之一它体现了分治思想Divide and Conquer的经典应用。这个由Tony Hoare在1959年发明的算法平均时间复杂度达到O(n log n)成为最有效率的通用排序方法之一。1.1 基础实现我们先看Python版本的快速排序实现def quick_sort(arr): if len(arr) 1: return arr pivot arr[len(arr) // 2] left [x for x in arr if x pivot] middle [x for x in arr if x pivot] right [x for x in arr if x pivot] return quick_sort(left) middle quick_sort(right)对应的Java实现则更注重原地排序in-place的特性public void quickSort(int[] arr, int low, int high) { if (low high) { int pi partition(arr, low, high); quickSort(arr, low, pi - 1); quickSort(arr, pi 1, high); } } private int partition(int[] arr, int low, int high) { int pivot arr[high]; int i low - 1; for (int j low; j high; j) { if (arr[j] pivot) { i; swap(arr, i, j); } } swap(arr, i 1, high); return i 1; } private void swap(int[] arr, int i, int j) { int temp arr[i]; arr[i] arr[j]; arr[j] temp; }1.2 边界条件与优化实际面试中面试官往往会追问各种边界情况的处理近乎有序的数组选择中间元素作为pivot可以避免最坏O(n²)复杂度大量重复元素三路快排将数组分为小于、等于和大于pivot三部分效率更高小规模数据当子数组长度小于某个阈值如15时切换为插入排序优化后的Python三路快排实现def quick_sort_3way(arr, low0, highNone): if high is None: high len(arr) - 1 if low high: return # 三路划分 lt, gt low, high pivot arr[low] i low while i gt: if arr[i] pivot: arr[lt], arr[i] arr[i], arr[lt] lt 1 i 1 elif arr[i] pivot: arr[gt], arr[i] arr[i], arr[gt] gt - 1 else: i 1 quick_sort_3way(arr, low, lt - 1) quick_sort_3way(arr, gt 1, high)1.3 复杂度分析快速排序的性能表现如下场景时间复杂度空间复杂度最优情况平衡划分O(n log n)O(log n)最差情况完全不平衡O(n²)O(n)平均情况O(n log n)O(log n)提示在实际工程中通常会采用随机化选择pivot的策略来避免最坏情况发生。2. LRU缓存数据结构的选择与权衡LRULeast Recently Used缓存淘汰算法是系统设计面试中的常客它要求在O(1)时间复杂度内完成get和put操作。要实现这一目标需要巧妙结合哈希表和双向链表两种数据结构。2.1 Python实现Python的OrderedDict已经内置了LRU功能但面试中通常要求自己实现class ListNode: def __init__(self, key0, val0): self.key key self.val val self.prev None self.next None class LRUCache: def __init__(self, capacity: int): self.capacity capacity self.cache {} self.head ListNode() self.tail ListNode() self.head.next self.tail self.tail.prev self.head def _remove(self, node): prev_node node.prev next_node node.next prev_node.next next_node next_node.prev prev_node def _add_to_head(self, node): node.prev self.head node.next self.head.next self.head.next.prev node self.head.next node def get(self, key: int) - int: if key not in self.cache: return -1 node self.cache[key] self._remove(node) self._add_to_head(node) return node.val def put(self, key: int, value: int) - None: if key in self.cache: node self.cache[key] node.val value self._remove(node) else: if len(self.cache) self.capacity: lru self.tail.prev self._remove(lru) del self.cache[lru.key] node ListNode(key, value) self.cache[key] node self._add_to_head(node)2.2 Java实现Java版本需要更显式地管理内存和指针操作class LRUCache { class DLinkedNode { int key; int value; DLinkedNode prev; DLinkedNode next; } private void addNode(DLinkedNode node) { node.prev head; node.next head.next; head.next.prev node; head.next node; } private void removeNode(DLinkedNode node) { DLinkedNode prev node.prev; DLinkedNode next node.next; prev.next next; next.prev prev; } private void moveToHead(DLinkedNode node) { removeNode(node); addNode(node); } private DLinkedNode popTail() { DLinkedNode res tail.prev; removeNode(res); return res; } private MapInteger, DLinkedNode cache new HashMap(); private int size; private int capacity; private DLinkedNode head, tail; public LRUCache(int capacity) { this.size 0; this.capacity capacity; head new DLinkedNode(); tail new DLinkedNode(); head.next tail; tail.prev head; } public int get(int key) { DLinkedNode node cache.get(key); if (node null) return -1; moveToHead(node); return node.value; } public void put(int key, int value) { DLinkedNode node cache.get(key); if (node null) { DLinkedNode newNode new DLinkedNode(); newNode.key key; newNode.value value; cache.put(key, newNode); addNode(newNode); size; if (size capacity) { DLinkedNode tail popTail(); cache.remove(tail.key); --size; } } else { node.value value; moveToHead(node); } } }2.3 复杂度与变种LRU缓存的性能特征操作时间复杂度空间复杂度getO(1)O(1)putO(1)O(1)整体O(1)O(capacity)实际工程中可能会考虑以下变种TTL支持为缓存项添加过期时间多级缓存结合本地缓存与分布式缓存LFU最不经常使用算法适合访问模式不均匀的场景3. K-Means聚类无监督学习的核心算法K-Means是聚类分析中最常用的算法之一它通过迭代将数据划分为K个簇使得每个数据点都属于离它最近的均值质心对应的簇。3.1 算法步骤随机选择K个点作为初始质心将每个数据点分配到最近的质心形成簇重新计算每个簇的质心均值重复步骤2-3直到质心不再显著变化或达到最大迭代次数3.2 Python实现import numpy as np class KMeans: def __init__(self, k3, max_iter100, tol1e-4): self.k k self.max_iter max_iter self.tol tol self.centroids None def fit(self, X): # 随机初始化质心 n_samples X.shape[0] random_indices np.random.choice(n_samples, self.k, replaceFalse) self.centroids X[random_indices] for _ in range(self.max_iter): # 分配样本到最近的质心 distances np.sqrt(((X - self.centroids[:, np.newaxis])**2).sum(axis2)) labels np.argmin(distances, axis0) # 更新质心 new_centroids np.array([X[labels i].mean(axis0) for i in range(self.k)]) # 检查收敛 if np.allclose(self.centroids, new_centroids, atolself.tol): break self.centroids new_centroids return self def predict(self, X): distances np.sqrt(((X - self.centroids[:, np.newaxis])**2).sum(axis2)) return np.argmin(distances, axis0)3.3 Java实现import java.util.ArrayList; import java.util.List; import java.util.Random; public class KMeans { private int k; private int maxIter; private double[][] centroids; public KMeans(int k, int maxIter) { this.k k; this.maxIter maxIter; } public void fit(double[][] X) { int n X.length; int dim X[0].length; // 随机初始化质心 centroids new double[k][dim]; Random rand new Random(); for (int i 0; i k; i) { int idx rand.nextInt(n); System.arraycopy(X[idx], 0, centroids[i], 0, dim); } for (int iter 0; iter maxIter; iter) { // 分配样本到最近的质心 int[] labels new int[n]; for (int i 0; i n; i) { double minDist Double.MAX_VALUE; for (int j 0; j k; j) { double dist 0; for (int d 0; d dim; d) { dist Math.pow(X[i][d] - centroids[j][d], 2); } if (dist minDist) { minDist dist; labels[i] j; } } } // 更新质心 double[][] newCentroids new double[k][dim]; int[] counts new int[k]; for (int i 0; i n; i) { int cluster labels[i]; for (int d 0; d dim; d) { newCentroids[cluster][d] X[i][d]; } counts[cluster]; } // 检查收敛 boolean converged true; for (int j 0; j k; j) { for (int d 0; d dim; d) { newCentroids[j][d] / counts[j]; if (Math.abs(newCentroids[j][d] - centroids[j][d]) 1e-4) { converged false; } } } if (converged) break; centroids newCentroids; } } public int[] predict(double[][] X) { int n X.length; int dim X[0].length; int[] labels new int[n]; for (int i 0; i n; i) { double minDist Double.MAX_VALUE; for (int j 0; j k; j) { double dist 0; for (int d 0; d dim; d) { dist Math.pow(X[i][d] - centroids[j][d], 2); } if (dist minDist) { minDist dist; labels[i] j; } } } return labels; } }3.4 优化与挑战K-Means算法有几个关键问题需要注意初始质心选择随机初始化可能导致局部最优可采用K-Means改进K值确定肘部法则Elbow Method或轮廓系数Silhouette Score可帮助选择最佳K数据标准化不同维度的量纲差异会影响聚类结果空簇处理当某个簇失去所有样本时需要重新分配质心优化后的K-Means初始化def initialize_centroids(X, k): centroids [X[np.random.choice(len(X))]] for _ in range(1, k): distances np.array([min([np.linalg.norm(x - c)**2 for c in centroids]) for x in X]) probabilities distances / distances.sum() centroids.append(X[np.random.choice(len(X), pprobabilities)]) return np.array(centroids)4. 算法比较与实战建议4.1 三种算法的对比特性快速排序LRU缓存K-Means算法类型排序算法缓存淘汰策略聚类算法时间复杂度O(n log n)O(1)O(nki)空间复杂度O(log n)O(n)O(nk)关键数据结构数组/递归栈哈希表双向链表向量空间稳定性不稳定N/AN/A应用场景通用排序缓存系统数据分组4.2 面试实战技巧快速排序能够解释分区过程的时间复杂度讨论如何选择pivot及其影响知道如何优化处理重复元素LRU缓存解释为什么需要双向链表讨论线程安全实现的可能性考虑分布式环境下的变种K-Means解释收敛条件和迭代过程讨论初始质心选择的影响知道如何评估聚类质量4.3 代码测试案例为每个算法准备测试案例非常重要# 快速排序测试 arr [3, 6, 8, 10, 1, 2, 1] sorted_arr quick_sort_3way(arr.copy()) print(f原始数组: {arr}) print(f排序结果: {sorted_arr}) # LRU缓存测试 lru LRUCache(2) lru.put(1, 1) lru.put(2, 2) print(lru.get(1)) # 返回1 lru.put(3, 3) # 淘汰键2 print(lru.get(2)) # 返回-1 # K-Means测试 from sklearn.datasets import make_blobs X, _ make_blobs(n_samples300, centers4, random_state42) kmeans KMeans(k4).fit(X) labels kmeans.predict(X) print(f前10个样本的聚类标签: {labels[:10]})在算法面试中理解这些经典实现的内在原理比死记硬背更重要。建议读者在理解上述代码后尝试自己从头实现并思考各种边界条件和优化可能性。真正的掌握来自于实践中的反复调试和优化。

更多文章