一、基本查找
基本查找通常指的是顺序查找(也叫线性查找)算法,这是最简单的查找算法。它的工作原理是,从数组的第一个元素开始,逐个检查每个元素,看它是否和我们要查找的元素匹配。如果发现匹配的元素,查找就结束了。
以下是一个简单的基本查找(线性查找)的Java实现:
public class BasicSearch {
public static int search(int[] arr, int x) {
int n = arr.length;
for (int i = 0; i < n; i++) {
if (arr[i] == x) {
return i; // 返回找到的元素的索引
}
}
return -1; // 如果找不到元素,返回-1
}
}
在这个函数中,我们遍历数组,检查每个元素是否与我们要查找的元素x匹配。如果找到匹配的元素,我们返回其索引;如果遍历完整个数组都没有找到匹配的元素,我们返回-1。
基本查找的时间复杂度是O(n),因为在最坏的情况下,我们需要检查数组中的每个元素。如果数组是有序的,我们可以使用更高效的查找算法,如二分查找,它的时间复杂度是O(log n)。然而,对于无序的数组,我们通常需要使用基本查找或其他类似的算法。
二、二分查找
二分查找(Binary Search)是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且同样在这一半的中间元素开始比较,直到找到要查找的元素,或者搜索范围为空。
以下是一个简单的二分查找的Java实现:
public class BinarySearch {
public static int binarySearch(int[] arr, int x) {
int low = 0;
int high = arr.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] == x) {
return mid; // 元素找到
} else if (arr[mid] < x) {
low = mid + 1; // 在右半边查找
} else {
high = mid - 1; // 在左半边查找
}
}
return -1; // 元素未找到
}
}
在这个函数中,我们首先找到数组的中间元素。如果中间元素是我们要查找的元素,我们就返回它的索引。如果中间元素比我们要查找的元素小,我们在数组的右半边查找;如果中间元素比我们要查找的元素大,我们在数组的左半边查找。我们重复这个过程,直到找到我们要查找的元素,或者搜索范围为空(即low > high)。
二分查找的时间复杂度是O(log n),它比基本查找(线性查找)更高效,但它需要数组是有序的。如果数组是无序的,我们需要先排序数组,然后才能使用二分查找,或者我们可以使用其他查找算法,如基本查找。
三、插值查找
插值查找基于一个观察:如果搜索键接近查找数组的起始位置,我们期望它比较早地被找到,相反,如果它接近数组的末尾,我们期望它较晚被找到。因此,插值查找动态地计算mid,使mid更接近搜索键的预期位置。
插值查找的mid的计算方式如下:
mid = low + ((key - arr[low]) / (arr[high] - arr[low])) * (high - low)
需要注意的是,插值查找的前提假设是数组是均匀分布的。如果数组的分布非常不均匀,插值查找可能不如二分查找效果好。
以下是插值查找的一个简单Java实现:
public class InterpolationSearch {
public static int interpolationSearch(int[] arr, int key) {
int low = 0;
int high = arr.length - 1;
while (low <= high && key >= arr[low] && key <= arr[high]) {
if (low == high) {
if (arr[low] == key) return low;
return -1;
}
int mid = low + ((key - arr[low]) / (arr[high] - arr[low])) * (high - low);
if (arr[mid] < key)
low = mid + 1;
else if (arr[mid] > key)
high = mid - 1;
else
return mid;
}
return -1;
}
}
在这个代码中,我们首先检查key是否在arr的范围内。如果是,我们就计算mid并比较arr[mid]和key。然后我们根据arr[mid]和key的比较结果更新low或high。如果arr[mid]等于key,我们就返回mid。如果在循环结束时还没找到key,我们就返回-1表示key不在数组中。
请注意这个实现假设输入数组已经排序,并且key在数组的范围内。在实际应用中,你可能需要对这个实现做适当的调整。
四、分块查找
分块查找(Block Search),又称索引顺序查找,是一种对线性查找的改进方法,用于查找有序的序列。它首先将列表划分为若干个块(子列表),并创建一个索引表来存储每个块的最大值和该块在主列表中的位置。在查找时,先用索引表确定包含目标元素的块,然后再在该块内进行顺序查找。
分块查找的步骤如下:
- 将列表划分为若干个块,并对每个块内的元素进行排序。
- 创建一个索引表,存储每个块的最大值和该块在主列表中的位置。
- 在索引表中查找包含目标元素的块。如果目标元素大于一个块的最大值且小于下一个块的最大值,那么目标元素就可能在这个块中。
- 在确定的块内进行顺序查找。
值得注意的是,分块查找适合于元素分布均匀的情况。如果元素分布非常不均,那么可能导致一些块非常大,这样就失去了分块查找的优势。
在Java中,我们可以创建一个索引表来存储每个块的最大值和其在主列表中的位置。然后使用这个索引表来确定包含目标元素的块,再在该块中进行顺序查找。以下是一个简单的实现:
import java.util.*;
public class BlockSearch {
static class Index {
int maxVal;
int start;
}
public static int blockSearch(int[] arr, int blockSize, int target) {
int numBlocks = (int) Math.ceil((double) arr.length / blockSize);
Index[] indices = new Index[numBlocks];
// 创建索引表
for (int i = 0; i < numBlocks; i++) {
int start = i * blockSize;
int end = Math.min(arr.length, start + blockSize);
int maxVal = Arrays.stream(arr, start, end).max().getAsInt();
indices[i] = new Index();
indices[i].maxVal = maxVal;
indices[i].start = start;
}
// 在索引表中找到可能包含目标元素的块
int blockIndex = -1;
for (int i = 0; i < numBlocks; i++) {
if (indices[i].maxVal >= target) {
blockIndex = i;
break;
}
}
if (blockIndex == -1) {
return -1;
}
// 在确定的块内进行顺序查找
int start = indices[blockIndex].start;
int end = Math.min(arr.length, start + blockSize);
for (int i = start; i < end; i++) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int blockSize = 3;
int target = 7;
System.out.println(blockSearch(arr, blockSize, target)); // 输出: 6
}
}
在这个示例中,我们首先计算出块的数量,然后创建一个索引表来存储每个块的最大值和其在主列表中的位置。然后我们使用这个索引表来确定可能包含目标元素的块,然后在这个块中进行顺序查找。
这个实现假设输入数组已经排序,并且块大小是固定的。在实际使用中,你可能需要根据具体情况调整这个实现。例如,如果输入数组没有排序,你可能需要先对每个块内的元素进行排序。或者,你可能需要根据数据的分布情况动态地确定块的大小。
五、哈希查找
哈希查找(Hashing)是一种非常高效的查找技术,它不是通过一步步的比较来查找目标值,而是通过一个函数(通常称为哈希函数或散列函数)直接计算出目标值在数据结构中的位置。这个位置通常称为哈希值。
哈希查找的基本步骤如下:
- 创建一个哈希表,这通常是一个数组结构。哈希表的大小通常比预期存储的元素数量大,以避免冲突(即不同的元素经过哈希函数计算得到相同的哈希值)。
- 通过哈希函数将元素的键值映射到哈希表中的一个位置。
- 当要查找一个元素时,只需将该元素的键值输入哈希函数,就可以直接计算出该元素在哈希表中的位置。
以下是一个简单的哈希查找的Java实现,使用Java的内置HashMap类:
import java.util.HashMap;
public class HashSearch {
public static void main(String[] args) {
// 创建一个哈希表
HashMap<Integer, String> map = new HashMap<>();
// 在哈希表中插入元素
map.put(1, "One");
map.put(2, "Two");
map.put(3, "Three");
// 查找键值为2的元素
String result = map.get(2);
System.out.println(result); // 输出: Two
}
}
在这个示例中,我们首先创建一个HashMap对象,然后在这个哈希表中插入几个元素。然后我们可以通过调用HashMap的get方法并传入一个键值,直接查找到对应的元素。
哈希查找的时间复杂度在理想情况下是O(1),这意味着无论哈希表中有多少元素,查找操作都只需要常数时间。然而,在实际应用中,由于可能会出现冲突,所以哈希查找的时间复杂度可能会稍微增加。解决冲突的一种常见方法是链地址法,即将所有哈希值相同的元素存储在一个链表中。
六、冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法。这个算法会一遍又一遍地遍历列表,比较每对相邻元素,如果它们的顺序错误就把它们交换过来。遍历列表的工作重复进行,直到没有再需要交换,也就是说列表已经排序完成。
冒泡排序的步骤如下:
-
从列表的第一个元素开始,比较当前元素和它的下一个元素。如果当前元素比下一个元素大,就交换这两个元素。这个过程对列表的每一对相邻元素都进行一遍,这样在第一轮结束时,最大的元素就会被移动到列表的最后。
-
对列表进行同样的操作,但是现在最后一个元素已经是最大的元素,所以不需要再处理它。这个过程对剩余的元素进行,然后对更少的元素进行,依此类推,直到只剩下一个元素,这个元素此时已经在正确的位置上。
下面是一个冒泡排序的Java实现:
public class BubbleSort {
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换 arr[j] 和 arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
冒泡排序的时间复杂度是O(n^2),在最好情况(已排序的输入)下是O(n)。由于其性能,这种排序算法通常只用于教学目的或处理小数据集。
请注意,尽管冒泡排序是一种简单的算法,但在实际应用中,我们通常会选择更有效率的排序算法,如快速排序、归并排序或堆排序。
七、选择排序
选择排序(Selection Sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
选择排序的步骤如下:
- 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
以下是一个简单的选择排序的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;
}
}
// 交换找到的最小元素和第 i 个元素
if (minIndex != i) {
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
}
}
这个Java函数首先从数组中查找最小的元素,并将其移动到正确的位置。然后,该函数对数组的剩余部分执行相同的操作,直到整个数组都被排序。
选择排序的时间复杂度在所有情况下都是O(n^2),因为该算法总是遍历整个输入来查找最小或最大元素。尽管选择排序在某些情况下可能比其他高级算法更有效(例如,当内存非常有限时),但在大多数情况下,其他算法如快速排序、归并排序或堆排序在效率上都优于选择排序。
八、插入排序
插入排序(Insertion Sort)是一种简单的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
插入排序的步骤如下:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果被扫描的元素(已排序)大于新元素,将该元素后移一位
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
以下是一个简单的插入排序的Java实现:
public class InsertionSort {
public static void insertionSort(int[] arr) {
int n = arr.length;
for (int i = 1; i < n; ++i) {
int key = arr[i];
int j = i - 1;
/* Move elements of arr[0..i-1], that are
greater than key, to one position ahead
of their current position */
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
}
}
在这个函数中,外部循环从数组的第二个元素开始遍历,将这个元素与其之前的所有元素进行比较。内部循环将这个元素向前移动,直到找到它在排序序列中的正确位置。
插入排序的时间复杂度在最坏情况下是O(n^2),当输入的数据已经是排序的情况下(最佳情况),插入排序的时间复杂度是O(n)。尽管插入排序对于较小的数组或者部分已排序的数组效率较高,但在处理大型数组时,其他更高效的排序算法(例如归并排序、快速排序或堆排序)通常是更好的选择。
九、递归算法
递归算法是一种编程技巧,它是指在算法中使用函数或方法直接或间接地调用自身的方式来解决问题。这种方法通常用于将复杂的问题分解为更简单的子问题,直到问题变得足够简单可以直接解决。
递归算法通常包括两个部分:
-
基本情况(Base case):这是递归结束的条件,当问题变得足够简单时,可以直接求解,而不需要继续递归。确保有一个有效的基本情况对于防止无限递归(无法终止的递归)非常重要。
-
递归情况(Recursive case):这是算法调用自身的部分,将问题细分为一个或多个子问题,然后继续递归求解这些子问题。通过逐步分解问题,直到达到基本情况,从而解决整个问题。
以下是一个用Java实现的计算阶乘的简单递归算法示例:
public static long factorial(int n) {
if (n == 0) { // 基本情况
return 1;
} else { // 递归情况
return n * factorial(n - 1);
}
}
在这个例子中,我们使用递归计算阶乘(n!)。基本情况是当n等于0时,阶乘的结果为1。递归情况是将问题细分为一个更小的问题(n * (n-1)!),然后继续调用factorial函数来求解。
递归思维
递归思维是一种解决问题的方式,它涉及到将一个大问题分解为更小、更易于处理的子问题,然后对这些子问题进行同样的处理,如此递归下去,直到问题被分解为可以直接解决的最小问题。
递归思维的关键步骤包括:
-
确定基本情况:这是递归应当停止的地方,也是问题可以被直接解决的地方。
-
确定递归情况:这是问题被分解为一个或多个更小问题的地方,这些更小的问题可以通过递归调用同样的过程来解决。
-
确定递归的结束:递归必须有一个明确的结束条件,否则它可能会无限制地运行下去。
例如,我们可以用递归思维来解决阶乘问题:
- 基本情况:如果n等于0,那么n的阶乘是1,我们可以直接返回结果。
- 递归情况:如果n大于0,那么n的阶乘是n乘以(n-1)的阶乘。我们可以通过递归调用同样的过程来计算(n-1)的阶乘。
通过这种方式,我们可以逐步分解问题,直到我们能够直接求解问题为止。这就是递归思维的核心。
十、快速排序
快速排序(Quick Sort)是一种常用的排序算法,由C. A. R. Hoare在1960年代早期发明。它的工作原理是使用分治策略(Divide and Conquer)来解决排序问题。
快速排序的基本思想是这样的:
-
分解:选择一个元素作为"基准"(pivot)。重新排列数组,所有比基准小的元素放在基准前面,所有比基准大的元素放在基准后面。在这个分区结束之后,基准就处在数组的中间位置。这个操作称为分区操作(Partition)。
-
解决:递归地对基准前后的两个子数组进行快速排序。
-
合并:因为子数组已经是排序好的,所以无需任何操作就能得到最终的排序结果。
以下是一个基于Lomuto划分方案简单的Java实现:
// 快速排序方法
public static void quickSort(int[] arr, int low, int high) {
// 如果low小于high,说明子序列中至少有两个元素
if (low < high) {
// 通过partition方法找到枢轴元素的正确位置pi
int pi = partition(arr, low, high);
// 递归对枢轴元素左侧的元素进行快速排序
quickSort(arr, low, pi - 1);
// 递归对枢轴元素右侧的元素进行快速排序
quickSort(arr, pi + 1, high);
}
}
// 找枢轴元素并进行划分的方法
private static int partition(int[] arr, int low, int high) {
// 选择最右侧的元素作为枢轴元素
int pivot = arr[high];
// 定义一个指针i,初始化为low-1,用于跟踪最新找到的小于枢轴的元素的位置
int i = low - 1;
// 从左到右扫描元素,找到小于枢轴的元素并与arr[i+1]交换
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
// 交换arr[i]和arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 最后,将枢轴元素放到正确的位置i+1,该位置左侧的元素都小于枢轴,右侧的元素都大于枢轴
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
// 返回枢轴的位置
return i + 1;
}
这个实现基于Lomuto划分方案,它选择最后一个元素作为枢轴,然后把小于枢轴的所有元素移到枢轴的左侧,大于枢轴的所有元素移到右侧,最后把枢轴元素移到中间。这样就把数组分为了两个部分,然后对这两个部分递归进行同样的操作,直到每个部分只剩下一个元素,这样整个数组就有序了。
以下是一个基于Hoare划分方案简单的Java实现:
public static void quickSort(int[] arr, int i, int j) {
// 初始化开始和结束的指针
int start = i;
int end = j;
// 如果开始指针大于结束指针,则返回
if (start > end) {
return;
}
// 选取基准数,这里选择的是数组的第一个元素
int baseNumber = arr[i];
// 当开始指针与结束指针还未相遇时,执行循环
while (start != end) {
// 从右向左查找第一个小于基准数的元素,找到后跳出循环
while (true) {
if (end <= start || arr[end] < baseNumber) {
break;
}
end--;
}
// 从左向右查找第一个大于基准数的元素,找到后跳出循环
while (true) {
if (end <= start || arr[start] > baseNumber) {
break;
}
start++;
}
// 交换开始指针和结束指针指向的元素
int temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
}
// 将基准数放到正确的位置,此时开始指针和结束指针相遇
int temp = arr[i];
arr[i] = arr[start];
arr[start] = temp;
// 递归对基准数左边的序列进行快速排序
quickSort(arr, i, start - 1);
// 递归对基准数右边的序列进行快速排序
quickSort(arr, start + 1, j);
}
这个实现基于Hoare划分方案,它首先选取一个基准数,然后从数组的两端开始向中间查找。从右向左找到第一个小于基准数的元素,从左向右找到第一个大于基准数的元素,然后交换这两个元素。重复这个过程,直到从两端开始的指针相遇。此时,指针相遇的位置左边的元素都小于基准数,右边的元素都大于基准数。然后将基准数和指针相遇的位置的元素交换,这样基准数就在正确的位置上了。然后对基准数左右两边的子序列分别递归进行同样的操作,直到每个子序列只有一个元素,这样整个数组就有序了。
Lomuto划分方案和Hoare划分方案都是用于快速排序中的划分算法,它们的主要任务是选取一个枢轴(pivot)并将数组的元素根据与枢轴的大小关系进行划分。这两种方案的主要区别在于选取枢轴的方式和执行划分的方式。
-
Lomuto划分方案:Lomuto划分方案选取数组中的一个元素作为枢轴,通常是数组的最后一个元素。然后从数组的左端开始扫描,将小于枢轴的元素都移到枢轴的左边,大于枢轴的元素都移到枢轴的右边。最后将枢轴元素放到正确的位置上。这个方案的实现比较直观简单,但是在处理有大量重复元素的数组时,其性能表现往往不如Hoare划分方案。
-
Hoare划分方案:Hoare划分方案选取数组中的一个元素作为枢轴,通常也是数组的一个元素,比如第一个元素或最后一个元素。然后同时从数组的两端开始扫描,从右向左找到第一个小于枢轴的元素,从左向右找到第一个大于枢轴的元素,然后交换这两个元素。重复这个过程,直到从两端开始的指针相遇。此时,指针相遇的位置左边的元素都小于枢轴,右边的元素都大于枢轴。然后将枢轴元素和指针相遇的位置的元素交换,这样枢轴元素就在正确的位置上了。这个方案的实现相对复杂一些,但是在处理有大量重复元素的数组时,其性能表现往往优于Lomuto划分方案。
在实际使用中,选择哪种划分方案主要取决于具体的应用场景和个人偏好。一般来说,如果追求代码的简洁性和易读性,可以选择Lomuto划分方案;如果追求性能,特别是在处理有大量重复元素的数组时,可以选择Hoare划分方案。
同时快速排序的平均时间复杂度是O(n log n),在最好情况下也是O(n log n),但在最坏情况下(输入数组已经排序)是O(n^2)。尽管如此,由于其在实际数据上的良好表现,快速排序仍然是最广泛使用的排序算法之一。