上一课时我们介绍了数据结构的知识,数据结构属于计算机存储的基础,有了它才能更好地将数据进行存储。而算法可以这样理解:它是为数据结构服务的,使用合适的算法可以更快地操作和查询这些数据。
算法的内容有很多,随随便便一本算法书有个 700 页到 1500 页也是很平常的事,因此我们在这里不能把所有的算法问题全部讲到,即使专门再开设一个算法专栏,也只能挑重点的讲。那么我们好钢就要用在刀刃上,本课时会把面试中经常出现的和平常工作中使用频率最高的算法,拿出来给大家分享。
我们本课时的面试题是,你知道哪些算法?讲一下它的内部实现?
#### 典型回答
最常见、最基础的算法是二分法,它是二分查找算法的简称(Binary Search Algorithm),也叫折半搜索算法或对数搜索算法。它是一种在有序数组中查找某一特定元素的搜索算法,顾名思义,是将一组有序元素中的数据划分为两组,通过判断中间值来确认要查找值的大致位置,然后重复此过程进行元素查询。
例如,我们要查询 1~100 中的某个数值,比如我们要查询的数值为 75,如果按照顺序从 1 开始一直往后排序对比的话,需要经历 75 次,才能查询到我们想要的数据;而如果使用二分法,则会先判断 50(1~100 的中间值)和 75 哪个大,然后就能确定要查询的值是在 50~100 之间,最后再进行二分,用 75 和 75 进行比较,结果发现此值就是我们想要找的那个值,于是我们只用了两步就找到了要查询的值,这就是算法的“魔力”。
![](https://img.kancloud.cn/84/39/8439377347fa81b9a1d80c0f4b4fdc80_558x264.png)
![](https://img.kancloud.cn/3e/21/3e21b8d33181881b4cf04386be14ee5f_578x236.png)
二分查找算法的实现代码如下:
```
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;
}
}
```
以上程序的执行结果为:
```
元素的位置是:75
```
从上面的内容我们可以看出二分法虽然简单,但是也要满足一个特定的条件才行,那就是要使用二分法必须是有序排列的数值才行,不然是没办法实现二分法的。
除了二分法之外还有另一个比较常用的排序算法:冒泡排序。
冒泡排序(Bubble Sort)又被称为泡式排序,它是指重复走访要排序的数列,每次比较两个元素,如果顺序不对就进行交换,直到没有被交换的元素为止,这样就完成了一次冒泡排序。
为了让大家更好地理解冒泡排序,我录制了一个 gif 图片用于展示冒泡排序的执行过程,如下图所示:
![](https://img.kancloud.cn/17/fe/17fe8dcc69b3b258e2b5b7bb8b89f575_451x278.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;
}
}
}
}
}
```
以上程序的执行结果为:
```
[11, 12, 22, 27, 33, 39, 45]
```
从以上结果可以看出,冒泡排序算法的执行成功了,结果也符合我们的预期。
#### 考点分析
冒泡排序和二分法属于程序员必须掌握的两种最基础的算法了,如果连这两个算法都不知道或者都不能手写这两种算法的话,可能会给面试官留下非常不好的印象。因为二者都属于基础中的基础了,其实只要理解了两种算法的核心思想,再手写代码也不是什么难事,如果实在写不出具体的代码,最起码要做到能写出伪代码的程度。
和此知识点相关的面试题,还有以下这些:
* 如何优化冒泡排序算法?
* 是否还知道更多的算法?
#### 知识扩展
* [ ] 冒泡排序优化
从上面冒泡排序的 gif 图片可以看出,在最后一轮对比之前,数组的排序如下图所示:
![](https://img.kancloud.cn/80/d1/80d1bdec40ae702457b7cbdf8c6db4be_846x514.png)
从图片可以看出,此时数组已经完全排序好了,但是即使这样,冒泡排序还是又执行了一次遍历对比,如下图所示:
![](https://img.kancloud.cn/17/fe/17fe8dcc69b3b258e2b5b7bb8b89f575_451x278.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;
}
}
}
}
```
以上程序的执行结果为:
```
[11, 12, 22, 27, 33, 39, 45]
```
此结果说明,冒泡排序的执行符合我们的预期,执行成功。
* [ ] 插入排序
插入排序(Insertion Sort)算法是指依次循环当前的列表,通过对比将每个元素插入到合适的位置,它的具体执行过程,如下图所示:
![](https://img.kancloud.cn/74/a7/74a7331f68d6a2173ccdcd77df469142_583x553.gif)
插入算法的具体实现代码如下:
```
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;
}
}
}
```
以上程序的执行结果为:
```
插入排序后:[4, 8, 10, 13, 20, 33, 49]
```
* [ ] 选择排序
选择排序(Selection Sort)算法是指依次循环数组,每轮找出最小的值放到数组的最前面,直到循环结束就能得到一个有序数组,它的执行过程如下图所示:
![](https://img.kancloud.cn/c2/83/c2832274b9eda42de6fdeef085b3ffd2_550x388.gif)
选择排序的具体实现代码如下:
```
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));
}
}
}
```
以上程序的执行结果为:
```
第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]
```
#### 小结
本着将一个知识点吃透的原则,本课时我们总共介绍了四种算法:冒泡排序算法、二分法、插入排序算法、选择排序算法等,并且还讲了冒泡排序算法的优化。但由于篇幅的原因,这些只能介绍一些常用的算法,如果想要更加深入地学习算法,还需要你投入更多的时间来学习,推荐阅读《算法》(第 4 版)的内容。
- 前言
- 开篇词
- 开篇词:大厂技术面试“潜规则”
- 模块一:Java 基础
- 第01讲:String 的特点是什么?它有哪些重要的方法?
- 第02讲:HashMap 底层实现原理是什么?JDK8 做了哪些优化?
- 第03讲:线程的状态有哪些?它是如何工作的?
- 第04讲:详解 ThreadPoolExecutor 的参数含义及源码执行流程?
- 第05讲:synchronized 和 ReentrantLock 的实现原理是什么?它们有什么区别?
- 第06讲:谈谈你对锁的理解?如何手动模拟一个死锁?
- 第07讲:深克隆和浅克隆有什么区别?它的实现方式有哪些?
- 第08讲:动态代理是如何实现的?JDK Proxy 和 CGLib 有什么区别?
- 第09讲:如何实现本地缓存和分布式缓存?
- 第10讲:如何手写一个消息队列和延迟消息队列?
- 模块二:热门框架
- 第11讲:底层源码分析 Spring 的核心功能和执行流程?(上)
- 第12讲:底层源码分析 Spring 的核心功能和执行流程?(下)
- 第13讲:MyBatis 使用了哪些设计模式?在源码中是如何体现的?
- 第14讲:SpringBoot 有哪些优点?它和 Spring 有什么区别?
- 第15讲:MQ 有什么作用?你都用过哪些 MQ 中间件?
- 模块三:数据库相关
- 第16讲:MySQL 的运行机制是什么?它有哪些引擎?
- 第17讲:MySQL 的优化方案有哪些?
- 第18讲:关系型数据和文档型数据库有什么区别?
- 第19讲:Redis 的过期策略和内存淘汰机制有什么区别?
- 第20讲:Redis 怎样实现的分布式锁?
- 第21讲:Redis 中如何实现的消息队列?实现的方式有几种?
- 第22讲:Redis 是如何实现高可用的?
- 模块四:Java 进阶
- 第23讲:说一下 JVM 的内存布局和运行原理?
- 第24讲:垃圾回收算法有哪些?
- 第25讲:你用过哪些垃圾回收器?它们有什么区别?
- 第26讲:生产环境如何排除和优化 JVM?
- 第27讲:单例的实现方式有几种?它们有什么优缺点?
- 第28讲:你知道哪些设计模式?分别对应的应用场景有哪些?
- 第29讲:红黑树和平衡二叉树有什么区别?
- 第30讲:你知道哪些算法?讲一下它的内部实现过程?
- 模块五:加分项
- 第31讲:如何保证接口的幂等性?常见的实现方案有哪些?
- 第32讲:TCP 为什么需要三次握手?
- 第33讲:Nginx 的负载均衡模式有哪些?它的实现原理是什么?
- 第34讲:Docker 有什么优点?使用时需要注意什么问题?
- 彩蛋
- 彩蛋:如何提高面试成功率?