Java 黑马程序员学习笔记(进阶篇9)
常见算法
1. 二分查找
(1) 前提条件:待查找的数组 必须是已排序的(通常为升序)。
(2) 核心思想:通过不断将查找区间一分为二,利用中间元素与目标值的比较,缩小查找范围,直到找到目标元素或确定区间为空(元素不存在)。
(3) 代码实现
public class BinarySearch {public static void main(String[] args) {// 前提:数组已按升序排序int[] arr = {11, 22, 33, 44, 55, 66, 77, 88};int target = 33;int result = binarySearch(arr, target);System.out.println("元素 " + target + " 的位置: " + result);}public static int binarySearch(int[] arr, int target) {int min = 0; // 左边界(初始为数组起始索引)int max = arr.length - 1; // 右边界(初始为数组末尾索引)// 当区间有效(左边界 ≤ 右边界)时,继续查找while (min <= max) {// 计算中间索引(推荐用 `min + (max - min) / 2` 避免整数溢出)int mid = (min + max) / 2;if (arr[mid] == target) {return mid; // 找到目标,返回元素索引} else if (arr[mid] < target) {// 中间元素 < 目标 → 目标在**右半段**,调整左边界min = mid + 1;} else {// 中间元素 > 目标 → 目标在**左半段**,调整右边界max = mid - 1;}}return -1; // 区间为空,未找到目标元素}
}
2. 分块查找
(1) 概念:
分块查找是一种结合顺序查找和二分查找优点的查找算法,核心思路是 “先分块、再定位”—— 把无序的大数据表,拆成若干个 “块”,满足 “块内无序、块间有序” 的规则,再通过 “索引表” 快速定位目标所在的块,最后在块内查找目标。
(2) 关键前提(必须满足)
- 块间有序:比如第 1 块的所有元素都小于第 2 块的元素,第 2 块都小于第 3 块(或反之,按升 / 降序约定一致);
- 块内无序:每个块内部的元素不需要排序(降低维护成本);
- 需额外维护 “索引表”:索引表记录每个块的 “关键信息”(通常是块的最大 / 最小值、块的起始索引、块的长度)。
(3) 分块查找两步流程(黑马经典拆解)
步骤 1:构建索引表
假设我们有一组数据:{8, 3, 12, 15, 20, 28, 25, 40, 35, 50, 48, 60}
,计划分 3 块(每块 4 个元素):
① 先给数据分块,确保块间有序:
- 第 1 块:
{8, 3, 12, 15}
→ 最大值 15; - 第 2 块:
{20, 28, 25, 40}
→ 最大值 40; - 第 3 块:
{35, 50, 48, 60}
→ 最大值 60;
(满足 “第 1 块最大值<第 2 块最大值<第 3 块最大值”,块间有序)
② 构建索引表:每个索引项对应一个块,包含 “块的最大值” 和 “块的起始索引”,如下:
索引项 | 块最大值 | 块起始索引 | 块长度 |
---|---|---|---|
0 | 15 | 0 | 4 |
1 | 40 | 4 | 4 |
2 | 60 | 8 | 4 |
步骤 2:查找目标元素(以找 “25” 为例)
阶段 1:定位目标所在的块(查索引表)
目标是 25,需找到 “块最大值≥25 且前一个块最大值<25” 的块;
索引表是有序的(块最大值递增),这里可选择两种方式查索引表:
- 若索引表小:用顺序查找(简单);
- 若索引表大:用二分查找(高效);
阶段 2:在目标块内查找(顺序查找)
- 第 1 块的数据是
{20, 28, 25, 40}
,块内无序,只能用顺序查找; - 遍历第 1 块,找到 25 在数组中的索引为 6,返回结果。
(4) 题目:分块查找算法实现
现有一个整数数组 arr = {16,5,9,12,21,18,32,23,37,26,45,34,50,48,61,52,73,66}
,该数组按照 “块内无序、块间有序” 的规则分为 3 个块,具体划分如下:
- 第 1 块:包含数组索引 0-5 的元素,最大值为 21
- 第 2 块:包含数组索引 6-11 的元素,最大值为 45
- 第 3 块:包含数组索引 12-17 的元素,最大值为 7
请完成以下任务:
① 设计一个 Block
类,用于存储每个块的关键信息:块的最大值(max)、块的起始索引(startIndex)、块的结束索引(endIndex),并提供必要的构造方法和 getter 方法。
② 实现分块查找算法,要求:
- 创建索引表(由
Block
对象组成的数组)管理上述 3 个块 - 编写
findIndexBlock
方法:根据目标值在索引表中定位其可能所在的块,返回该块在索引表中的索引(若目标值大于所有块的最大值,返回 - 1) - 编写
getIndex
方法:利用findIndexBlock
定位的块,在该块对应的数组范围内查找目标值,返回目标值在原始数组中的索引(若未找到,返回 - 1)
③ 使用目标值 30
测试上述实现,输出查找结果(若找到则返回索引,否则返回 - 1)
package demo4;public class test8 {public static void main(String[] args) {int[] arr = {16,5,9,12,21,18,32,23,37,26,45,34,50,48,61,52,73,66};Block b1 = new Block(21,0,5);Block b2 = new Block(45,6,11);Block b3 = new Block(73,12,17);//定义索引表来管理三个块的对象Block[] blockArr = {b1,b2,b3};int number = 30;int index = getIndex(blockArr,arr,number);}public static int getIndex(Block[] blockArr, int[] arr, int number) { //这里的参数不太理解int indexBlock = findIndexBlock(blockArr, number);if (indexBlock == -1) {return -1;}int startIndex = blockArr[indexBlock].getStartIndex();int endIndex = blockArr[indexBlock].getEndIndex();for (int i = startIndex; i <= endIndex; i++) {if (arr[i] == number) {return i;}}return -1;}public static int findIndexBlock(Block[] blockArr, int number) { //这里的参数不太理解for (int i = 0; i < blockArr.length; i++) {if (number <= blockArr[i].getMax()) { //不太理解,这里的getMax()是什么意思return i;}}return -1;}
}class Block {private int max;private int startIndex;private int endIndex;public Block() {}public Block(int max, int startIndex, int endIndex) {this.max = max;this.startIndex = startIndex;this.endIndex = endIndex;}public int getMax() {return max;}public void setMax(int max) {this.max = max;}public int getStartIndex() {return startIndex;}public void setStartIndex(int startIndex) {this.startIndex = startIndex;}public int getEndIndex() {return endIndex;}public void setEndIndex(int endIndex) {this.endIndex = endIndex;}
}
关键逻辑 1:blockArr[i].getMax()
的含义
要理解这个方法,必须先看Block
类的结构 —— 它是一个 “封装块信息的模板”:
class Block {private int max; // 块的最大值(私有属性,外部不能直接访问)private int startIndex; // 块的起始索引private int endIndex; // 块的结束索引// getMax()是“ getter方法 ”——专门用来获取私有属性max的值public int getMax() {return max;}// 其他getter/setter方法...
}
所以:
blockArr[i]
→ 表示 “索引表中第 i 个块”(比如blockArr[0]
是b1
,blockArr[1]
是b2
);
blockArr[i].getMax()
→ 表示 “获取索引表中第 i 个块的最大值”(比如blockArr[1].getMax()
就是b2
的最大值 45)。
关键逻辑 2:getIndex
方法的参数
参数名 | 类型 | 含义与作用 |
---|---|---|
blockArr | Block[] | 这是 “索引表”—— 存储所有块的关键信息(每个元素是一个Block 对象,包含块的最大值、起始索引、结束索引)。作用:通过它能拿到 “目标块” 的起始 / 结束索引(比如找到目标在第 2 块,就从 blockArr[1] 里拿该块的startIndex 和endIndex )。 |
arr | int[] | 这是 “原始数据数组”—— 我们要查找目标值的数据源。 作用:当确定 “目标块” 的范围后(比如起始索引 6、结束索引 11),需要遍历 arr 的这个范围,找到目标值的具体位置。 |
number | int | 这是 “目标查找值”—— 我们最终要在arr 中找到的数字(比如代码里的 30)。作用:既是 “定位目标块” 的依据,也是 “块内查找” 时的匹配条件。 |
3. 冒泡排序
(1) 概念:
冒泡排序的核心思想是 “相邻元素两两比较,大值逐步‘沉底’(移到数组末尾)”,每一轮排序都会将当前未排序区间的 “最大值” 通过多次交换,移动到未排序区间的末尾,如同水中的气泡逐渐上浮(小值靠前)、大值沉底(大值靠后),因此得名 “冒泡”。
(2) 关键特征
- 比较规则:仅比较相邻的两个元素;
- 交换规则:若前一个元素 > 后一个元素(升序排序),则交换两者位置;
- 轮次规律:数组长度为
n
时,最多需要n-1
轮排序(每轮沉底 1 个大值,最后 1 个元素无需比较)。
(3) 代码实现
package demo4;public class test9 {public static void main(String[] args) {int[] arr = {2, 4, 5, 3, 1};for (int i = 0; i < arr.length - 1; i++) {for (int j = 0; j < arr.length - 1 - i; j++) {if (arr[j] > arr[j + 1]) {int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}printArr(arr);}private static void printArr(int[] arr) {for (int i = 0; i < arr.length; i++) {System.out.print(arr[i] + " ");}}
}
关键逻辑 1:外层循环控制排序的 “轮数”
for (int i = 0; i < arr.length - 1; i++)
循环条件i < arr.length - 1
的原因:
- 数组长度为
n
(如代码中arr.length=5
)时,最多需要n-1
轮排序。因为每轮会确定 1 个元素的最终位置(沉底),当n-1
个元素排好后,最后 1 个元素自然也在正确位置,无需再排序。 - 例如:数组长度 5,只需 4 轮(
i=0
到i=3
)即可完成排序。
关键逻辑 2:内层循环控制每轮的 “比较次数”
for (int j = 0; j < arr.length - 1 - i; j++)
① 循环条件j < arr.length - 1 - i
的原因:
每轮排序后,已有i
个最大值 “沉底”(排在数组末尾,位置固定),这些元素无需再参与比较。因此 “未排序区间” 的长度会随i
增大而减小,每轮的比较次数也随之减少。
② 具体来说:arr.length - 1
:保证j+1
不越界(因为要比较arr[j]
和arr[j+1]
);
4. 选择排序
① 原理:
选择排序是一种基于 “选择最值” 的基础排序算法,核心思想可概括为:
“每轮从待排序区间中找到‘最小值’(或最大值),将其与待排序区间的‘第一个元素’交换位置,逐步缩小待排序区间,直到整个数组有序”。
② 关键特征:
- 区间划分:数组分为 “待排序区间” 和 “已排序区间”(初始时已排序区间为空,待排序区间为整个数组);
- 最值选择:每轮仅遍历待排序区间,找到最值的索引(而非频繁交换);
- 交换时机:每轮仅交换 1 次 —— 将最值与待排序区间的第一个元素交换,使最值进入已排序区间;
- 轮次规律:数组长度为
n
时,需n-1
轮排序(最后 1 个元素无需比较,自然有序)。
package demo3;public class test7 {public static void main(String[] args) {int[] arr = {2, 4, 5, 3, 1};for (int i = 0; i < arr.length - 1; i++) {for (int j = i + 1; j < arr.length; j++) {if (arr[i] > arr[j]) {int temp = arr[i];arr[i] = arr[j];arr[j] = temp;}}}printArr(arr);}private static void printArr(int[] arr) {for (int i = 0; i < arr.length; i++) {System.out.print(arr[i] + " ");}}
}
关键逻辑 1:外层循环控制排序的 “轮数”
for (int i = 0; i < arr.length - 1; i++)
循环条件i < arr.length - 1
的原因:
- 数组长度为
n
(如代码中arr.length=5
)时,最多需要n-1
轮排序。 - 例如:数组长度 5,
i
从 0 到 3(共 4 轮)即可完成排序,最后一个元素(索引 4)自动有序。
关键逻辑 2:内层循环遍历 “待排序区间”,找最小值并交换
for (int j = i + 1; j < arr.length; j++)
内层循环的核心作用:从待排序区间(i+1
到末尾)中,找到比arr[i]
小的元素,通过交换让arr[i]
成为待排序区间中的最小值(即已排序区间的新末尾)。
j = i + 1
:待排序区间的起点是i+1
(因为i
及左侧是已排序区间,无需再比较);j < arr.length
:待排序区间的终点是数组末尾(arr.length - 1
),需要遍历到最后一个元素。