Java八股文面试题 基础篇 -- 二分查找算法冒泡排序选择排序插入排序希尔排序快速排序
Posted CodeJiao
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java八股文面试题 基础篇 -- 二分查找算法冒泡排序选择排序插入排序希尔排序快速排序相关的知识,希望对你有一定的参考价值。
1. 二分查找算法
要求
- 能够用自己语言描述二分查找算法
- 能够手写二分查找代码
- 能够解答一些变化后的考法
1.1 二分查找算法介绍
二分查找也是一种在数组中查找数据的算法。它只能查找已经排好序的数据。二分查找通过比较数组中间的数据与目标数据的大小,可以得知目标数据是在数组的左边还是右边。因此,比较一次就可以把查找范围缩小一半。重复执行该操作就可以找到目标数据,或得出目标数据不存在的结论。
现在我们来试试查找6这个元素。
首先找到数组中间的数字,此处为5。
将5和要查找的数字6进行比较。
把不需要的数字移出查找范围。
在剩下的数组中找到中间的数字,此处为7。
比较7和6。
把不需要的数字移出查找范围。
在剩下的数组中找到中间的数字,此处为6。
6=6,成功找到目标数字。
1.2 二分查找说明
1.3 算法描述
-
前提:有已排序数组 array(假设已经做好)
-
定义左边界 lIndex、右边界 rIndex,确定搜索范围,循环执行二分查找(3、4两步)
-
获取中间索引 mIndex = Floor((lIndex+rIndex) /2)
-
中间索引的值 array[mIndex] 与待搜索的值 target 进行比较
① array[mIndex] == target 表示找到,返回中间索引
②array[mIndex] > target,中间值右侧的其它元素都大于 target,无需比较,中间索引左边去找,mIndex - 1 设置为右边界,重新查找
③ array[mIndex] < target,中间值左侧的其它元素都小于 target,无需比较,中间索引右边去找, mIndex + 1 设置为左边界,重新查找
-
当 lIndex > rIndex 时,表示没有找到,应结束循环
1.4 算法实现
/**
* 二分查找算法
*
* @param array 待查找的数据
* @param target 目标元素
* @return target在array中的索引值,如果没有匹配的元素则返-1。
*/
public static int binarySearch(int[] array, int target)
int lIndex = 0, rIndex = array.length - 1, mIndex;
while (lIndex <= rIndex)
/*
* 无符号右移动1位,相当于 /2。
* 这样可以防止lIndex + rIndex溢出整数最大值。
* 因为移位运算符是接近于计算机硬件的,所以相比于 / 效率也会有所提高。
*/
mIndex = (lIndex + rIndex) >>> 1;
if (target == array[mIndex])
return mIndex;
else if (target < array[mIndex])
rIndex = mIndex - 1;
else
lIndex = mIndex + 1;
// 如果没有匹配的元素则返-1
return -1;
测试代码:
public static void main(String[] args)
int[] array = 1, 5, 8, 11, 19, 22, 31, 35, 40, 45, 48, 49, 50;
int target = 8;
int idx = binarySearch(array, target);
System.out.println(idx);
运行结果:
1.5 相关面试题
1.6 二分查找小结
2. 冒泡排序(稳定)
要求
- 能够用自己语言描述冒泡排序算法
- 能够手写冒泡排序代码
- 了解一些冒泡排序的优化手段
2.1 冒泡排序介绍
在序列的最右边放置一个天平,比较天平两边的数字。如果右边的数字较小,就交换这两个数字的位置。
由于6<7,所以交换这两个数字。
完成后,天平往左移动一个位置,比较两个数字的大小。此处4<6,所以无须交换。
继续将天平往左移动一个位置并比较数字。重复同样的操作直到天平到达序列最左边为止。
不断对数字进行交换,天平最终到达了最左边。通过这一系列操作,序列中最小的数字就会移动到最左边。
最左边的数字已经归位。
将天平移回最右边,然后重复之前的操作,直到天平到达左边第2个位置为止。
当天平到达左边第2个位置时,序列中第2小的数字也就到达了指定位置。
将天平再次移回最右边,重复同样的操作直到所有数字都归位为止。
排序中……
排序中……
排序完成。
2.2 冒泡排序说明
2.3 算法描述
- 依次比较数组中相邻两个元素大小,若 array[j] > array[j+1],则交换两个元素,两两都比较一遍称为一轮冒泡,结果是让最大的元素排至最后。
- 重复以上步骤,直到整个数组有序。
2.4 算法实现
/**
* 冒泡排序
*
* @param array 待排序的数组
*/
public static void bubbleSort(int[] array)
int n = array.length - 1;
while (true)
int last = 0; // 表示最后一次交换索引位置
for (int i = 0; i < n; i++)
// 如果 array[i] > array[i + 1] 则交换这2个元素的位置 目的是把较大的元素冒泡到后面
if (array[i] > array[i + 1])
int temp = array[i];
array[i] = array[i + 1];
array[i + 1] = temp;
last = i;
// last 则位最近一次冒泡最后交换位置的起始索引i,因为i后面都是排好序的 所以不需要排序
n = last;
// 如果上一次没有发生交换,则数组已经排好顺序,可以退出外层循环了
if (n == 0)
break;
测试代码:
public static void main(String[] args)
int[] array = 1, 5, 8, 11, 19, 22, 31, 35, 40, 45, 48, 49, 50;
bubbleSort(array);
System.out.println(Arrays.toString(array));
运行结果:
3. 选择排序(不稳定)
要求
- 能够用自己语言描述选择排序算法
- 能够比较选择排序与冒泡排序
- 理解非稳定排序与稳定排序
3.1 选择排序介绍
选择排序就是重复 从待排序的数据中寻找最小值,将其与序列最左边的数字进行交换 这一操作的算法。在序列中寻找最小值时使用的是线性查找。
对数字1~9进行排序。
使用线性查找在数据中寻找最小值,于是我们找到了最小值1(线性查找的详细说明在3-1节)。
将最小值1与序列最左边的6进行交换,最小值1归位。不过,如果最小值已经在最左端,就不需要任何操作。
在余下的数据中继续寻找最小值。这次我们找到了最小值2。
将数字2与左边第2个数字6进行交换,最小值2归位。
重复同样的操作直到所有数字都归位为止。
排序完成。
3.2 选择排序说明
3.3 选择排序算法描述
-
将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选出最小的元素,放入排序子集
-
重复以上步骤,直到整个数组有序
3.4 选择排序代码实现
/**
* 选择排序算法实现
*
* @param array 待排序数组
*/
public static void selection(int[] array)
// 选择排序需要排序 n - 1 轮
for (int i = 0; i < array.length - 1; i++)
/*
* i 代表每轮选择最小元素要交换到的目标索引
* m 代表每轮最小元素的索引
*/
int m = i;
for (int j = i + 1; j < array.length; j++)
/*
* j 元素比 元素还要小, 更新 m
*/
if (array[m] > array[j])
m = j;
/*
* 优化点:为减少交换次数,每一轮可以先找最小的索引,在每轮最后再交换元素
*/
if (m != i)
int temp = array[m];
array[m] = array[i];
array[i] = temp;
测试代码:
public static void main(String[] args)
int[] array = 1, 5, 8, 11, 19, 22, 31, 35, 40, 45, 48, 49, 50;
selection(array);
System.out.println(Arrays.toString(array));
3.5 选择排序与冒泡排序比较
- 二者平均时间复杂度都是 O ( n 2 ) O(n^2) O(n2)。
- 选择排序一般要快于冒泡,因为其交换次数少。
- 但如果集合有序度高,冒泡优于选择。
- 冒泡属于稳定排序算法,而选择属于不稳定排序。
- 稳定排序指,按对象中不同字段进行多次排序,不会打乱同值元素的顺序。
- 不稳定排序则反之。
4. 插入排序(稳定)
要求
- 能够用自己语言描述插入排序算法
- 能够比较插入排序与选择排序
4.1 插入排序介绍
插入排序是一种从序列左端开始依次对数据进行排序的算法。在排序过程中,左侧的数据陆续归位,而右侧留下的就是还未被排序的数据。插入排序的思路就是从右侧的未排序区域内取出一个数据,然后将它插入到已排序区域内合适的位置上。
此处对数字1~9进行排序。
首先,我们假设最左边的数字5已经完成排序,所以此时只有5是已归位的数字。
接下来,从待排数字(未排序区域)中取出最左边的数字3,将它与左边已归位的数字进行比较。若左边的数字更大,就交换这两个数字。重复该操作,直到左边已归位的数字比取出的数字更小,或者取出的数字已经被移到整个序列的最左边为止。
由于5>3,所以交换这两个数字。
对数字3的操作到此结束。此时3和5已归位,还剩下右边7个数字尚未排序。
接下来是第3轮。和前面一样,取出未排序区域中最左边的数字4,将它与左边的数字5进行比较。
由于5>4,所以交换这两个数字。交换后再把4和左边的3进行比较,发现3<4,因为出现了比自己小的数字,所以操作结束。
于是4也归位了。此时3、4、5都已归位,已排序区域也得到了扩大。
遇到左边的数字都比自己小的情况时……
不需要任何操作即可完成排序。
重复上述操作,直到所有数字都归位。
对所有数字的操作都结束时,排序也就完成了。
4.2 插入排序说明
4.3 插入排序算法描述
-
将数组分为两个区域,排序区域和未排序区域,每一轮从未排序区域中取出第一个元素,插入到排序区域(需保证顺序)。
-
重复以上步骤,直到整个数组有序。
4.4 插入排序代码实现
/**
* 插入排序代码实现
*
* @param a 待排序的数组
*/
public static void insertSort(int[] a)
// i 代表待插入元素的索引
for (int i = 1; i < a.length; i++)
// temp 代表待插入的元素值
int temp = a[i];
int j = i;
while (j >= 1)
// a[j-1] 是a[i]上一个元素索引,如果 > temp,后移
if (temp < a[j - 1])
a[j] = a[j - 1];
j--;
else
// 如果 a[j-1] 已经 <= t, 则 j 就是插入位置
break;
a[j] = temp;
测试代码:
public static void main(String[] args)
int[] array = 1, 5, 8, 11, 19, 22, 31, 35, 40, 45, 48, 49, 50;
insertSort(array);
System.out.println(Arrays.toString(array));
运行结果:
4.5 插入排序与选择排序比较
- 二者平均时间复杂度都是 O ( n 2 ) O(n^2) O(n2)
- 大部分情况下,插入都略优于选择
- 插入属于稳定排序算法,而选择属于不稳定排序
提示
插入排序通常被同学们所轻视,其实它的地位非常重要。小数据量排序,都会优先选择插入排序
5. 希尔排序(插入排序的改进算法)
要求
- 能够用自己语言描述希尔排序算法
5.1 算法描述
-
首先选取一个间隙序列,如 (n/2,n/4 … 1),n 为数组长度
-
每一轮将间隙相等的元素视为一组,对组内元素进行插入排序,目的有二
① 少量元素插入排序速度很快
② 让组内值较大的元素更快地移动到后方
-
当间隙逐渐减少,直至为 1 时,即可完成排序
5.2 代码实现
private static void shell(int[] a)
int n = a.length;
for (int gap = n / 2; gap > 0; gap /= 2)
// i 代表待插入元素的索引
for (int i = gap; i < n; i++)
int t = a[i]; // 代表待插入的元素值
int j = i;
while (j >= gap)
// 每次与上一个间隙为 gap 的元素进行插入排序
if (t < a[j - gap]) // j-gap 是上一个元素索引,如果 > t,后移
a[j] = a[j - gap];
j -= gap;
else // 如果 j-1 已经 <= t, 则 j 就是插入位置
break;
a[j] = t;
System.out.println(Arrays.toString(a) + " gap:" + gap);
6. 快速排序
要求
- 能够用自己语言描述快速排序算法
- 掌握手写单边循环、双边循环代码之一
- 能够说明快排特点
- 了解洛穆托与霍尔两种分区方案的性能比较
6.1 快速排序介绍
快速排序算法首先会在序列中随机选择一个基准值(pivot),然后将除了基准值以外的数分为“比基准值小的数”和“比基准值大的数”这两个类别,再将其排列成以下形式。
[比基准值小的数] 基准值 [比基准值大的数]
接着,对两个“[ ]”中的数据进行排序之后,整体的排序便完成了。对“[ ]”里面的数据进行排序时同样也会使用快速排序。
下面我们就来看看快速排序的步骤。
在序列中随机选择一个基准值。这里选择了4。
将其他数字和基准值进行比较。小于基准值的往左移,大于基准值的往右移。
首先,比较3和基准值4。
因为3<4,所以将3往左移。
接下来,比较5和基准值4。
因为5>4,所以将5往右移。
对其他数字也进行同样的操作,最终结果如上图所示。
把基准值4插入序列。这样,4左边就是比它小的数字,右边就是比它大的数字。
分别对左边和右边的数据进行排序后,就能完成整体的排序。
随机选择一个基准值。这次选择6。
把其余数据分别和基准值6进行比较,小于基准值的就往左移,大于的就往右移。
完成了大小比较和位置移动。
和前面一样,对左右两边分别进行排序,进而完成整体排序。但是此时左边只有5,所以已经是排序完成的状态,不需要任何操作。而右边就和前面一样,先选出基准值。
选择8作为基准值。
将9和7分别与基准值8进行比较后,两个数字的位置便分好了。8的两边都只有一个数据,因此不需要任何操作。这样7、8、9便完成排序了。
回到上一行,由于7、8、9完成了排序,所以5、6、7、8、9也完成了排序。
于是,最初选择的基准值4的右边排序完毕。
左边也以相同的操作进行排序,整体的排序工作也就完成了。
6.2 快速排序说明
6.3 算法描述
- 每一轮排序选择一个基准点(pivot)进行分区
- 让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区
- 当分区完成时,基准点元素的位置就是其最终位置
- 在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 (divide-and-conquer)
- 从以上描述可以看出,一个关键在于分区算法,常见的有洛穆托分区方案、双边循环分区方案、霍尔分区方案
6.4 快速排序代码实现
单边循环快排(lomuto 洛穆托分区方案)
-
选择最右元素作为基准点元素
-
j 指针负责找到比基准点小的元素,一旦找到则与 i 进行交换
-
i 指针维护小于基准点元素的边界,也是每次交换的目标索引
-
最后基准点与 i 交换,i 即为分区位置
public static void quick(int[] a, int l, int h)
if (l >= h)
return;
int p = partition(a, l, h); // p 索引值
quick(a, l, p - 1); // 左边分区的范围确定
quick(a, p + 1, h); // 左边分区的范围确定
private static int partition(int[] a, int l, int h)
int pv = a[h]; // 基准点元素
int i = l;
for (int j = l; j < h; j++)
if (a[j] < pv)
if (i != j)
swap(a, i, j);
i++;
if (i != h)
swap(a, h, i);
System.out.println(Arrays.toString(a) + " i=" + i);
// 返回值代表了基准点元素所在的正确索引,用它确定下一轮分区的边界
return i;
双边循环快排(不完全等价于 hoare 霍尔分区方案)
- 选择最左元素作为基准点元素
- j 指针负责从右向左找比基准点小的元素,i 指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至 i,j 相交
- 最后基准点与 i(此时 i 与 j 相等)交换,i 即为分区位置
要点
-
基准点在左边,并且要先 j 后 i
-
while( i < j && a[j] > pv ) j--
-
while ( i< j && a[i] <=pv ) i++
private static void quick(int[] a, int l, int h)
if (l >= h)
return;
int p = partition(a, l, h);
quick(a, l, p - 1);
quick(a, p + 1, h);
private static int partition(intJava面试题-基础篇