在本文中,我们将学习一些基础的排序和搜索算法,包括:
- 冒泡排序(Bubble Sort)
- 选择排序(Selection Sort)
- 插入排序(Insertion Sort)
- 线性搜索(Linear Search)
- 跳跃搜索(Jump Search)
- 二分搜索(Binary Search)
排序算法
冒泡排序(Bubble Sort)
冒泡排序是最简单的排序算法之一。它重复地遍历要排序的列表,比对每对相邻的元素,如果它们的顺序错误就交换它们。遍历列表并交换的过程会一直重复到列表已经排序完成。
在冒泡排序中,每次遍历其实都会将列表的末尾第 n 个元素排好序(n 为当前遍历的次数,因此,冒泡排序最大也只需要 n 次循环)。如果我们把一个数组“竖”过来看并把每个元素当作一个气泡,那么排序过程就像是将对应的气泡向上浮,直到它们按照大小顺序排列(因此得名冒泡排序)。
可视化示例
在下面的可视化示例中,我们将演示冒泡排序的工作原理。我们将使用一个仅仅包含 4 个元素的数组 [6, 3, 0, 5]
来演示。
第一次遍历
在第一次遍历中,我们首先比较第一个元素 6
和第二个元素 3
,由于 6 > 3
,我们交换它们的位置。此时,数组变为 [3, 6, 0, 5]
。
然后,我们比较第二个元素 6
和第三个元素 0
,由于 6 > 0
,我们再次交换它们的位置。此时,数组变为 [3, 0, 6, 5]
。
然后,我们比较第三个元素 6
和第四个元素 5
,由于 6 > 5
,我们再次交换它们的位置。此时,数组变为 [3, 0, 5, 6]
。
此时,第一次遍历结束,数组变为 [3, 0, 5, 6]
,同时我们会发现数组的最大元素 6
已经被排到了最后。
第二次遍历
在第二次遍历中,我们首先比较第一个元素 3
和第二个元素 0
,由于 3 > 0
,我们交换它们的位置。此时,数组变为 [0, 3, 5, 6]
。
然后,我们比较第二个元素 3
和第三个元素 5
,由于 3 < 5
,我们不需要交换它们的位置。此时,数组保持不变 [0, 3, 5, 6]
。
然后,我们比较第三个元素 5
和第四个元素 6
,由于 5 < 6
,我们不需要交换它们的位置。此时,数组保持不变 [0, 3, 5, 6]
。
此时,第二次遍历结束,数组变为 [0, 3, 5, 6]
,同时我们会发现数组的第二大元素 5
已经被排到了倒数第二的位置,最大的元素 6
仍然在最后。
第三次遍历
在第三次遍历中,我们首先比较第一个元素 0
和第二个元素 3
,由于 0 < 3
,我们不需要交换它们的位置。此时,数组保持不变 [0, 3, 5, 6]
。
然后,我们比较第二个元素 3
和第三个元素 5
,由于 3 < 5
,我们不需要交换它们的位置。此时,数组保持不变 [0, 3, 5, 6]
。
然后,我们比较第三个元素 5
和第四个元素 6
,由于 5 < 6
,我们不需要交换它们的位置。此时,数组保持不变 [0, 3, 5, 6]
。
此时,第三次遍历结束,数组变为 [0, 3, 5, 6]
,同时我们会发现数组的第三大元素 3
已经被排到了倒数第三的位置。
第四次遍历
在第四次遍历中,我们首先比较第一个元素 0
和第二个元素 3
,由于 0 < 3
,我们不需要交换它们的位置。此时,数组保持不变 [0, 3, 5, 6]
。
然后,我们比较第二个元素 3
和第三个元素 5
,由于 3 < 5
,我们不需要交换它们的位置。此时,数组保持不变 [0, 3, 5, 6]
。
然后,我们比较第三个元素 5
和第四个元素 6
,由于 5 < 6
,我们不需要交换它们的位置。此时,数组保持不变 [0, 3, 5, 6]
。
此时,第四次遍历结束,数组变为 [0, 3, 5, 6]
,同时我们会发现数组的第四大元素 0
已经被排到了倒数第四的位置。
因此,冒泡排序的最终结果是 [0, 3, 5, 6]
。
你发现了吗?
但是,其实有的时候我们并不需要真的遍历 n 遍数组。例如,在上述示例中,我们的第三次和第四次遍历都没有发生任何交换,这意味着数组已经是有序的了。因此,我们可以在每次遍历中记录是否发生了交换,如果没有发生交换,我们就可以提前结束排序。
代码示例
下面是冒泡排序的 Java 代码示例:
1 | import java.util.ArrayList; |
选择排序(Selection Sort)
选择排序是另一种简单的排序算法。它的工作原理是:
- 我们知道数组的第一个元素是最小的。因此,我们先在数组中找到最小的元素,并将其与第一个元素交换。
- 然后,我们知道数组的第一个元素已经是最小的了,因此我们在剩下的元素中找到第二小的元素,并将其与第二个元素交换。
- 以此类推,直到所有元素都被排序。
可视化示例
在下面的可视化示例中,我们将演示选择排序的工作原理。我们仍然使用 [6, 3, 0, 5]
这个数组。
- 第一步,我们需要找到数组中最小的元素并将其与第一个元素交换。在这个例子中,最小的元素是
0
,因此我们将0
与第一个元素6
交换,数组变为[0, 3, 6, 5]
。 - 第二步,我们需要找到剩下的元素中最小的元素并将其与第二个元素交换。在这个例子中,剩下的元素是
[3, 6, 5]
,最小的元素是3
,因此我们将3
与第二个元素6
交换,数组变为[0, 3, 6, 5]
。 - 第三步,我们需要找到剩下的元素中最小的元素并将其与第三个元素交换。在这个例子中,剩下的元素是
[6, 5]
,最小的元素是5
,因此我们将5
与第三个元素6
交换,数组变为[0, 3, 5, 6]
。 - 第四步,我们需要找到剩下的元素中最小的元素并将其与第四个元素交换。在这个例子中,剩下的元素是
[6]
,最小的元素是6
,因此我们不需要交换,数组保持不变[0, 3, 5, 6]
。
因此,选择排序的最终结果是 [0, 3, 5, 6]
。
代码示例
1 | class SelectionSort { |
插入排序(Insertion Sort)
插入排序是一种简单的排序算法,它的工作原理是:
- 我们创建一个单独的数组,用于存储已经排序好的元素。
- 我们从原数组中取出一个元素,然后将其插入到已排序数组的正确位置。
- 如果已排序数组为空,我们直接将这个元素插入到已排序数组中。
- 如果已排序数组不为空
- 如果这个元素比已排序数组的第一个元素小,我们将这个元素插入到已排序数组的第一个位置。
- 否则,我们从已排序数组的第一个元素开始,逐个比较这个元素与已排序数组中的元素,直到找到一个比这个元素大的元素,然后将这个元素插入到这个元素的前面。
可视化示例
在下面的可视化示例中,我们将演示插入排序的工作原理。我们仍然使用 [6, 3, 0, 5]
这个数组。
- 第一步,我们从原数组中取出第一个元素
6
。由于目前已排序数组为空,我们直接将6
插入到已排序数组中,已排序数组变为[6]
。 - 第二步,我们要取出的元素是
3
。由于3
比已排序数组中的6
小,我们将3
插入到已排序数组的第一个位置,已排序数组变为[3, 6]
。 - 第三步,我们要取出的元素是
0
。由于0
比已排序数组中的3
小,我们将0
插入到已排序数组的第一个位置,已排序数组变为[0, 3, 6]
。 - 第四步,我们要取出的元素是
5
。由于5
比已排序数组中的3
大,但是比6
小,我们将5
插入到已排序数组的第二个位置,已排序数组变为[0, 3, 5, 6]
。
因此,插入排序的最终结果是 [0, 3, 5, 6]
。
代码示例
1 | class InsertionSort { |
搜索算法
线性搜索(Linear Search)
线性搜索是最简单的搜索算法。它的工作原理是:从数组的第一个元素开始,逐个比较每个元素,直到找到目标元素或者遍历完整个数组。
线性搜索时数组/列表可以是无序的。
代码示例
1 | class LinearSearch { |
跳跃搜索(Jump Search)
跳跃搜索是一种更高效的搜索算法。它的工作原理是:
- 我们需要确定要搜索的数组是有序的。
- 我们首先确定一个“跳跃步长”(jump step),通常是数组长度的平方根。
- 然后,我们从数组的第一个元素开始,每次跳跃步长,直到找到一个元素,这个元素比目标元素大。
- 然后,我们知道我们要找的元素一定在本次跳跃的范围内,然后我们可以使用线性搜索来搜索这个范围。
可视化
我们在这里会使用 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]
这个有序数组来演示跳跃搜索的工作原理。
我们要搜索的目标是 55
。
- 首先,我们确定跳跃步长为 4(改数组长度为 16,平方根为 4)。
- 我们从数组的第一个元素
0
开始,每次跳跃 4 个元素,直到找到一个元素比目标元素55
大。- 我们首先检查第一个元素
0
。由于0 < 55
,我们继续跳跃。 - 然后,我们跳跃到了下标为
0 + 4 = 4
的元素3
。由于3 < 55
,我们继续跳跃。 - 然后,我们跳跃到了下标为
4 + 4 = 8
的元素21
。由于21 < 55
,我们继续跳跃。 - 然后,我们跳跃到了下标为
8 + 4 = 12
的元素144
。由于144 > 55
,我们停止跳跃。
- 我们首先检查第一个元素
- 此时,我们知道目标元素
55
一定在下标8
和12
之间,然后我们使用线性搜索在这个范围内搜索目标元素。 - 我们找到了目标元素
55
,并返回它的索引10
。
代码示例
1 | class JumpSearch { |
二分搜索(Binary Search)
二分搜索是一种更高效的搜索算法。它的工作原理是:
- 我们需要确定要搜索的数组是有序的。
- 我们首先确定数组的中间元素。
- 如果目标元素等于中间元素,我们找到了目标。
- 如果目标元素小于中间元素,由于数组是有序的,我们知道目标元素一定在中间元素的左侧。
- 如果目标元素大于中间元素,目标元素一定在中间元素的右侧。
- 然后我们减少搜索范围到左侧到中间或者中间到右侧,重复上述步骤,直到找到目标元素或者搜索范围为空(如果搜索范围为空,说明目标元素不存在)。
可视化
我们这里仍然使用 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]
这个有序数组来演示二分搜索的工作原理。
我们要搜索的目标是 89
。
- 首先,我们确定数组的中间元素是
21
(数组长度为 16,中间元素是下标为8
的元素)。 - 由于
21 < 89
,我们知道目标元素一定在中间元素的右侧。 - 此时,我们减少搜索范围到
[34, 55, 89, 144, 233, 377, 610]
。 - 我们再次确定中间元素是
144
(数组现在长度为 7,中间元素是下标为3
的元素)。 - 由于
144 > 89
,我们知道目标元素一定在中间元素的左侧。 - 此时,我们减少搜索范围到
[34, 55, 89]
。 - 我们再次确定中间元素是
55
(数组现在长度为 3,中间元素是下标为1
的元素)。 - 由于
55 < 89
,我们知道目标元素一定在中间元素的右侧。 - 此时,我们减少搜索范围到
[89]
。 - 我们找到了目标元素
89
,并返回它的索引。不过索引需要考虑到原始数组以及我们减少的搜索范围,所以最终的索引是11
。
代码示例
1 | class BinarySearch { |
GL & HF!