常见的算法你知道哪些?
# 前言
数据结构属于计算机存储的基础,有了它才能更好地将数据进行存储。而算法可以这样理解:它是为数据结构服务的,使用合适的算法可以更快地操作和查询这些数据。
算法的内容有很多,随随便便一本算法书有个 700 页到 1500 页也是很平常的事,因此我们在这里不能把所有的算法问题全部讲到,即使专门再开设一个算法专栏,也只能挑重点的讲。那么我们好钢就要用在刀刃上,本文会把面试中经常出现的和平常工作中使用频率最高的算法,拿出来给大家分享。
# 你知道哪些算法?讲一下它的内部实现?
# 二分查找
最常见、最基础的算法是二分法,它是二分查找算法的简称(Binary Search Algorithm),也叫折半搜索算法或对数搜索算法。它是一种在有序数组中查找某一特定元素的搜索算法,顾名思义,是将一组有序元素中的数据划分为两组,通过判断中间值来确认要查找值的大致位置,然后重复此过程进行元素查询。
例如,我们要查询 1~100 中的某个数值,比如我们要查询的数值为 75,如果按照顺序从 1 开始一直往后排序对比的话,需要经历 75 次,才能查询到我们想要的数据;而如果使用二分法,则会先判断 50(1~100 的中间值)和 75 哪个大,然后就能确定要查询的值是在 50~100 之间,最后再进行二分,用 75 和 75 进行比较,结果发现此值就是我们想要找的那个值,于是我们只用了两步就找到了要查询的值,这就是算法的“魔力”。
二分查找算法的实现代码如下:
public class AlgorithmExample {
public static void main(String[] args) {
// 要查询的数组
int[] binaryNums = new int[100];
for (int i = 0; i < 100; i++) {
// 初始化数组(存入 100 个数据)
binaryNums[i] = (i + 1);
}
// 要查询的数值
int findValue = 75;
// 调用二分查找算法
int binaryResult = binarySearch(binaryNums, 0, binaryNums.length - 1, findValue);
// 打印结果
System.out.println("元素的位置是:" + (binaryResult + 1));
}
/**
* 二分查找算法(返回该值第一次出现的位置)
* @param nums 查询数组
* @param start 开始下标
* @param end 结束下标
* @param findValue 要查找的值
* @return int
*/
private static int binarySearch(int[] nums, int start, int end, int findValue) {
if (start <= end) {
// 中间位置
int middle = (start + end) / 2;
// 中间的值
int middleValue = nums[middle];
if (findValue == middleValue) {
// 等于中值直接返回
return middle;
} else if (findValue < middleValue) {
// 小于中值,在中值之前的数据中查找
return binarySearch(nums, start, middle - 1, findValue);
} else {
// 大于中值,在中值之后的数据中查找
return binarySearch(nums, middle + 1, end, findValue);
}
}
return -1;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
以上程序的执行结果为:
元素的位置是:75
从上面的内容我们可以看出二分法虽然简单,但是也要满足一个特定的条件才行,那就是要使用二分法必须是有序排列的数值才行,不然是没办法实现二分法的。
# 冒泡排序
除了二分法之外还有另一个比较常用的排序算法:冒泡排序。
冒泡排序(Bubble Sort)又被称为泡式排序,它是指重复走访要排序的数列,每次比较两个元素,如果顺序不对就进行交换,直到没有被交换的元素为止,这样就完成了一次冒泡排序。
为了让大家更好地理解冒泡排序,我录制了一个 gif 图片用于展示冒泡排序的执行过程,如下图所示:
由上图可以看出,冒泡排序的关键执行流程是:依次对比相邻的两个数字,如果前面的数字大于后面的数字,那么就将前、后两个数字进行位置交换;这样每次对比完一轮数据之后,能找出此轮最大的数字并放置到尾部,如此重复,直到没有可以交换的数据为止,这样就完成了冒泡排序。
接下来我们就使用 Java 代码来实现一个冒泡排序算法,实现代码如下:
import java.util.Arrays;
public class AlgorithmExample {
public static void main(String[] args) {
// 待排序数组
int[] nums = {33, 45, 11, 22, 12, 39, 27};
bubbleSort(nums);
// 打印排序完数组
System.out.println(Arrays.toString(nums));
}
/**
* 冒泡排序
*/
private static void bubbleSort(int[] nums) {
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < nums.length - i; j++) {
if (nums[j] > nums[j + 1]) {
// 前面数字大于后面的数字,执行位置交换
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
以上程序的执行结果为:
[11, 12, 22, 27, 33, 39, 45]
从以上结果可以看出,冒泡排序算法的执行成功了,结果也符合我们的预期。
# 考点分析
冒泡排序和二分法属于程序员必须掌握的两种最基础的算法了,如果连这两个算法都不知道或者都不能手写这两种算法的话,可能会给面试官留下非常不好的印象。因为二者都属于基础中的基础了,其实只要理解了两种算法的核心思想,再手写代码也不是什么难事,如果实在写不出具体的代码,最起码要做到能写出伪代码的程度。
面试官可能还会这样问:
如何优化冒泡排序算法?
是否还知道更多的算法?
# 知识扩展
# 冒泡排序优化
从上面冒泡排序的 gif 图片可以看出,在最后一轮对比之前,数组的排序如下图所示:
从图片可以看出,此时数组已经完全排序好了,但是即使这样,冒泡排序还是又执行了一次遍历对比,如下图所示:
因此我们就可以想办法去掉无效的遍历,这样就可以优化冒泡排序的执行效率了。
我们可以在第一层循环内加一个判断标识,每次赋值为 true,假如在第二层循环(内层循环)时执行了位置交换,也就是 if 中的代码之后,我们把此值设置成 false;如果执行完内层循环判断之后,变量依然为 true,这就说明没有可以移动的元素了,冒泡排序可以结束执行了,优化后的代码如下所示:
import java.util.Arrays;
public class AlgorithmExample {
public static void main(String[] args) {
// 待排序数组
int[] nums = {33, 45, 11, 22, 12, 39, 27};
bubbleSort(nums);
// 打印排序完数组
System.out.println(Arrays.toString(nums));
}
/**
* 冒泡排序
*/
private static void bubbleSort(int[] nums) {
for (int i = 1; i < nums.length; i++) {
// 判断标识
boolean flag = true;
for (int j = 0; j < nums.length - i; j++) {
if (nums[j] > nums[j + 1]) {
// 前面数字大于后面的数字,执行位置交换
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
// 执行了位置交换,更改标识
flag = false;
}
}
if (flag) {
// 没有可以移动的元素了,跳出循环
break;
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
以上程序的执行结果为:
[11, 12, 22, 27, 33, 39, 45]
此结果说明,冒泡排序的执行符合我们的预期,执行成功。
# 插入排序
插入排序(Insertion Sort)算法是指依次循环当前的列表,通过对比将每个元素插入到合适的位置,它的具体执行过程,如下图所示:
插入算法的具体实现代码如下:
public class AlgorithmExample {
public static void main(String[] args) {
int[] insertNums = {4, 33, 10, 13, 49, 20, 8};
// 插入排序调用
insertSort(insertNums);
System.out.println("插入排序后:" + Arrays.toString(insertNums));
}
/**
* 插入排序
*/
private static void insertSort(int[] nums) {
int i, j, k;
for (i = 1; i < nums.length; i++) {
k = nums[i];
j = i - 1;
// 对 i 之前的数据,给当前元素找到合适的位置
while (j >= 0 && k < nums[j]) {
nums[j + 1] = nums[j];
// j-- 继续往前寻找
j--;
}
nums[j + 1] = k;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
以上程序的执行结果为:
插入排序后:[4, 8, 10, 13, 20, 33, 49]
# 选择排序
选择排序(Selection Sort)算法是指依次循环数组,每轮找出最小的值放到数组的最前面,直到循环结束就能得到一个有序数组,它的执行过程如下图所示:
选择排序的具体实现代码如下:
public class AlgorithmExample {
public static void main(String[] args) {
int[] insertNums = {4, 33, 10, 13, 49, 20, 8};
// 调用选择排序
selectSort(insertNums);
System.out.println("选择排序后结果:" + Arrays.toString(insertNums));
}
/**
* 选择排序
*/
private static void selectSort(int[] nums) {
int index;
int temp;
for (int i = 0; i < nums.length - 1; i++) {
index = i;
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] < nums[index]) {
index = j;
}
}
if (index != i) {
temp = nums[i];
nums[i] = nums[index];
nums[index] = temp;
}
System.out.print("第" + i + "次排序:");
System.out.println(Arrays.toString(nums));
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
以上程序的执行结果为:
第0次排序:[4, 33, 10, 13, 49, 20, 8]
第1次排序:[4, 8, 10, 13, 49, 20, 33]
第2次排序:[4, 8, 10, 13, 49, 20, 33]
第3次排序:[4, 8, 10, 13, 49, 20, 33]
第4次排序:[4, 8, 10, 13, 20, 49, 33]
第5次排序:[4, 8, 10, 13, 20, 33, 49]
选择排序后结果:[4, 8, 10, 13, 20, 33, 49]
2
3
4
5
6
7
# 总结
本文总共介绍了四种算法:冒泡排序算法、二分法、插入排序算法、选择排序算法等,并且还讲了冒泡排序算法的优化。但由于篇幅的原因,这些只能介绍一些常用的算法,如果想要更加深入地学习算法,还需要你投入更多的时间来学习,推荐阅读《算法》(第 4 版)的内容。