1. 什么是快速排序
- 定义
快速排序(Quick Sort)是对冒泡排序的一种改进,它采用分治法(Divide and Conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。该算法选择一个基准值(pivot),将数组分为两部分,使得左边部分的所有元素都小于等于基准值,右边部分的所有元素都大于等于基准值,然后分别对左右两部分递归地进行快速排序,最终使整个数组有序。
- 要点
- 基准值选择:基准值的选取对排序性能有较大影响,常见的选择方式有选第一个元素、最后一个元素、中间元素或者随机元素作为基准值。
- 分区操作:这是快速排序的核心,通过分区将数组分为两部分,使得左边部分元素小于等于基准值,右边部分元素大于等于基准值。
- 递归排序:对分区后的左右两部分分别递归调用快速排序算法,直到子数组不可再分。
- 应用
- 大规模数据排序:在处理大规模无序数据时,快速排序平均时间复杂度为 O(nlogn),能高效完成排序任务。例如数据库中对大量数据记录按照某个字段进行排序。
- 编程语言内置排序函数:很多编程语言的内置排序函数在一定条件下会采用快速排序算法,如 Python 的
sorted()
函数在部分情况下使用优化后的快速排序。
示例代码
java
public class QuickSort {
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pivotIndex = partition(arr, low, high);
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}
private static 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 static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
2. 什么是直接选择排序
- 定义
直接选择排序(Straight Selection Sort)是一种简单的排序算法。它的基本思想是在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
- 要点
- 选择最小(大)元素:每次遍历未排序序列,找出最小(大)元素的位置。
- 交换元素:将找到的最小(大)元素与未排序序列的第一个元素交换位置。
- 应用
- 数据量较小的排序场景:由于直接选择排序的时间复杂度为 O(n^2),在数据量较小时,实现简单且性能损失可接受。比如对班级里少量学生的成绩进行排序。
- 对排序稳定性要求不高的场景:直接选择排序是不稳定的排序算法,当对排序稳定性没有严格要求时可以使用。
示例代码
java
public class SelectionSort {
public static void selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
swap(arr, i, minIndex);
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
3. 什么是堆排序
- 定义
堆排序(Heap Sort)是指利用堆这种数据结构所设计的一种排序算法。堆是一种完全二叉树,分为大顶堆和小顶堆。大顶堆的每个节点的值都大于或等于其子节点的值,小顶堆则相反。堆排序的基本步骤是先将数组构建成一个大顶堆,然后将堆顶元素(最大值)与数组的最后一个元素交换,再对剩余的元素重新调整为大顶堆,重复这个过程直到数组有序。
- 要点
- 堆的构建:从最后一个非叶子节点开始,依次调整每个节点,使其满足堆的性质。
- 堆的调整:在交换堆顶元素后,需要对剩余元素重新调整为大顶堆。
- 应用
- 优先队列的实现:堆排序的思想可用于实现优先队列,优先队列中插入和删除操作都能保持高效。例如任务调度系统中,根据任务的优先级进行调度。
- 海量数据中找 Top K 元素:可以使用堆排序在海量数据中找出最大或最小的 K 个元素。
示例代码
java
public class HeapSort {
public static void heapSort(int[] arr) {
int n = arr.length;
// 构建大顶堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 一个个交换元素
for (int i = n - 1; i > 0; i--) {
swap(arr, 0, i);
heapify(arr, i, 0);
}
}
private static void heapify(int[] arr, int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, n, largest);
}
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
4. 什么是归并排序
- 定义
归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法,该算法采用分治法(Divide and Conquer)。它将一个数组分成两个子数组,分别对这两个子数组进行归并排序,然后将排好序的子数组合并成一个有序的数组。
- 要点
- 分割数组:不断将数组分成两半,直到每个子数组只有一个元素。
- 合并子数组:将两个有序的子数组合并成一个有序的数组,合并过程需要额外的存储空间。
- 应用
- 外部排序:处理大规模数据时,当数据无法一次性加载到内存中,可将数据分成多个小块,分别排序后再合并。例如对大型文件进行排序。
- 对排序稳定性有要求的场景:归并排序是稳定的排序算法,当需要保证相等元素的相对顺序不变时可以使用。
示例代码
java
public class MergeSort {
public static void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
private static void merge(int[] arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int[] L = new int[n1];
int[] R = new int[n2];
for (int i = 0; i < n1; i++) {
L[i] = arr[left + i];
}
for (int j = 0; j < n2; j++) {
R[j] = arr[mid + 1 + j];
}
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
}
5. 什么是基数排序
- 定义
基数排序(Radix Sort)是一种非比较排序算法,它根据数字的每一位来进行排序。从最低位开始,依次对每一位进行排序,直到最高位。通过多次排序,最终使整个数组有序。
- 要点
- 按位排序:从最低位到最高位,依次对每一位进行排序。
- 使用桶:通常使用 10 个桶来存储每一位上的数字。
- 应用
- 整数排序:对于整数数据,基数排序能高效完成排序,尤其在数据范围不大且位数固定的情况下。
- 字符串排序:可以按照字符的 ASCII 码值进行排序,常用于对字符串数组进行排序。
示例代码
java
import java.util.Arrays;
public class RadixSort {
public static void radixSort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
int max = getMax(arr);
for (int exp = 1; max / exp > 0; exp *= 10) {
countingSort(arr, exp);
}
}
private static int getMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
private static void countingSort(int[] arr, int exp) {
int n = arr.length;
int[] output = new int[n];
int[] count = new int[10];
Arrays.fill(count, 0);
for (int i = 0; i < n; i++) {
count[(arr[i] / exp) % 10]++;
}
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
for (int i = n - 1; i >= 0; i--) {
output[count[(arr[i] / exp) % 10] - 1] = arr[i];
count[(arr[i] / exp) % 10]--;
}
for (int i = 0; i < n; i++) {
arr[i] = output[i];
}
}
}
6. 什么是二分查找树
- 定义
二分查找树(Binary Search Tree,BST)是一种二叉树,对于树中的每个节点,其左子树中的所有节点的值都小于该节点的值,右子树中的所有节点的值都大于该节点的值。利用这个性质,可以在 O(logn) 的平均时间复杂度内进行查找、插入和删除操作。
- 要点
- 节点比较:插入、查找和删除操作都基于节点值的比较。
- 递归操作:大多数操作都可以通过递归的方式实现。
- 应用
- 字典和集合的实现:可以用于实现字典和集合等数据结构,提供高效的查找、插入和删除操作。
- 数据库索引:在数据库中,二分查找树可以作为索引结构,提高数据的查询效率。
示例代码
java
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
public class BinarySearchTree {
private TreeNode root;
public void insert(int val) {
root = insertRec(root, val);
}
private TreeNode insertRec(TreeNode root, int val) {
if (root == null) {
return new TreeNode(val);
}
if (val < root.val) {
root.left = insertRec(root.left, val);
} else if (val > root.val) {
root.right = insertRec(root.right, val);
}
return root;
}
public boolean search(int val) {
return searchRec(root, val);
}
private boolean searchRec(TreeNode root, int val) {
if (root == null || root.val == val) {
return root != null;
}
if (val < root.val) {
return searchRec(root.left, val);
}
return searchRec(root.right, val);
}
}
7. 什么是 LRU 实现
- 定义
LRU(Least Recently Used)即最近最少使用,是一种缓存淘汰策略。当缓存满时,会优先淘汰最近最少使用的数据。通常使用哈希表和双向链表来实现,哈希表用于快速查找数据,双向链表用于维护数据的访问顺序。
- 要点
- 哈希表:存储键值对,实现 O(1) 的查找时间复杂度。
- 双向链表:维护数据的访问顺序,最近访问的数据放在链表头部,最久未访问的数据放在链表尾部。
- 应用
- 操作系统的页面置换算法:当物理内存不足时,操作系统可以使用 LRU 算法淘汰最近最少使用的页面。
- 数据库的缓存管理:数据库系统可以使用 LRU 算法管理缓存,提高数据的访问效率。
示例代码
java
import java.util.HashMap;
import java.util.Map;
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
public class LRUCache {
private Map<Integer, 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(key, value);
cache.put(key, newNode);
addToHead(newNode);
size++;
if (size > capacity) {
DLinkedNode removed = removeTail();
cache.remove(removed.key);
size--;
}
} else {
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode removed = tail.prev;
removeNode(removed);
return removed;
}
}
8. 判断一个数是不是整数
- 定义
判断一个数是否为整数,就是确定该数是否没有小数部分,或者是否属于 int
、long
等整数类型。
- 要点
- 类型判断:使用
instanceof
关键字判断对象是否为整数类型。 - 小数部分判断:对于浮点数,通过取整后与原数比较来判断是否有小数部分。
- 应用
- 数据验证:在进行数据处理时,需要验证输入的数据是否为整数,例如在用户输入年龄、数量等信息时。
- 数学计算:在某些数学计算中,需要确保操作数为整数,避免出现意外的结果。
示例代码
java
public class IntegerCheck {
public static boolean isInteger(double num) {
return num == (int) num;
}
public static boolean isInteger(Object obj) {
return obj instanceof Integer;
}
}
9. 红黑树与二叉树有什么区别、红黑树用途
- 定义
- 红黑树:是一种自平衡的二叉搜索树,每个节点都带有颜色属性(红色或黑色)。通过对节点颜色的约束和旋转操作,保证树的高度始终保持在 O(logn),从而保证插入、删除和查找操作的时间复杂度都是 O(logn)。
- 二叉树:是一种树形数据结构,每个节点最多有两个子节点,但不保证树的平衡性,最坏情况下,插入、删除和查找操作的时间复杂度可能达到 O(n)。
- 要点
- 红黑树的性质:根节点是黑色;每个叶子节点(NIL 节点,空节点)是黑色;如果一个节点是红色的,则它的子节点必须是黑色的;对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
- 平衡维护:红黑树通过旋转和颜色调整来维护平衡,而普通二叉树不进行平衡维护。
- 应用
- Java 集合框架:
TreeMap
和TreeSet
底层使用红黑树实现,保证元素的有序性和高效的插入、删除和查找操作。 - 操作系统:用于实现文件系统的目录结构,提高查找效率。
10. 什么是 AVL 树,有哪四种旋转方式,举个 AVL 树左右旋转的例子
- 定义
AVL 树是一种自平衡的二叉搜索树,它在每个节点中维护一个平衡因子(该节点的左子树高度减去右子树高度),平衡因子的取值只能是 -1、0 或 1。当插入或删除节点导致某个节点的平衡因子超出这个范围时,需要通过旋转操作来恢复树的平衡。
- 四种旋转方式
- 左旋转(Left Rotation):用于处理右右失衡的情况,将一个节点的右子树提升为根节点,原根节点变为新根节点的左子树。
- 右旋转(Right Rotation):用于处理左左失衡的情况,将一个节点的左子树提升为根节点,原根节点变为新根节点的右子树。
- 左右旋转(Left - Right Rotation):用于处理左右失衡的情况,先对左子树进行左旋转,再对根节点进行右旋转。
- 右左旋转(Right - Left Rotation):用于处理右左失衡的情况,先对右子树进行右旋转,再对根节点进行左旋转。
- 应用
- 数据库索引:AVL 树能保证高效的查找、插入和删除操作,可用于数据库的索引结构,提高数据的查询效率。
- 编译器的符号表:在编译器中,AVL 树可以用于实现符号表,快速查找和管理标识符。
- 左右旋转例子
假设我们有一个 AVL 树,插入一个节点后导致根节点的左子树的右子树高度增加,出现左右失衡的情况。
初始 AVL 树:
plaintext
5
/
3
\
4
插入节点 2 后,树变为:
plaintext
5
/
3
/ \
2 4
此时根节点 5 的平衡因子为 2,左子树 3 的平衡因子为 -1,出现左右失衡。
首先对左子树 3 进行左旋转:
plaintext
5
/
4
/
3
\
2
然后对根节点 5 进行右旋转:
plaintext
4
/ \
3 5
/
2
经过左右旋转后,树恢复平衡。
示例代码
java
class AVLTreeNode {
int val;
int height;
AVLTreeNode left;
AVLTreeNode right;
AVLTreeNode(int val) {
this.val = val;
this.height = 1;
}
}
public class AVLTree {
private AVLTreeNode root;
private int height(AVLTreeNode node) {
if (node == null) {
return 0;
}
return node.height;
}
private int getBalance(AVLTreeNode node) {
if (node == null) {
return 0;
}
return height(node.left) - height(node.right);
}
private AVLTreeNode rightRotate(AVLTreeNode y) {
AVLTreeNode x = y.left;
AVLTreeNode T2 = x.right;
x.right = y;
y.left = T2;
y.height = Math.max(height(y.left), height(y.right)) + 1;
x.height = Math.max(height(x.left), height(x.right)) + 1;
return x;
}
private AVLTreeNode leftRotate(AVLTreeNode x) {
AVLTreeNode y = x.right;
AVLTreeNode T2 = y.left;
y.left = x;
x.right = T2;
x.height = Math.max(height(x.left), height(x.right)) + 1;
y.height = Math.max(height(y.left), height(y.right)) + 1;
return y;
}
public AVLTreeNode insert(AVLTreeNode node, int val) {
if (node == null) {
return new AVLTreeNode(val);
}
if (val < node.val) {
node.left = insert(node.left, val);
} else if (val > node.val) {
node.right = insert(node.right, val);
} else {
return node;
}
node.height = 1 + Math.max(height(node.left), height(node.right));
int balance = getBalance(node);
// 左左情况
if (balance > 1 && val < node.left.val) {
return rightRotate(node);
}
// 右右情况
if (balance < -1 && val > node.right.val) {
return leftRotate(node);
}
// 左右情况
if (balance > 1 && val > node.left.val) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
// 右左情况
if (balance < -1 && val < node.right.val) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
}
友情提示:本文已经整理成文档,可以到如下链接免积分下载阅读