二分查找算法,也称折半搜索算法,是一种在有序数组中查找某一特定元素的搜索算法
public class BinarySearch {
public static void main(String[] args) {
// 数组
int[] array = {1, 5, 8, 11, 19, 22, 31, 35, 40, 45, 48, 49, 50};
// 待查找元素
// int target = 48;
int target = 47;
int idx = binarySearch(array, target);
System.out.println(idx);
}
// 二分查找, 找到返回元素索引, 找不到返回-1
private static int binarySearch(int[] array, int target) {
// 1. 前提:有已排序数组 A
// 2. 定义左边界 L、右边界 R,确定搜索范围,循环执行二分查找(3、4两步)
int l = 0, r = array.length - 1, m;
while (l <= r) {
// 3. 获取中间索引 M = Floor((L+R) /2)
m = (l + r ) / 2;;
// 4. 中间索引的值 A[M] 与待搜索的值 T 进行比较
if(array[m] == target) {
// ① A[M] == T 表示找到,返回中间索引
return m;
} else if(array[m] > target) {
// ② A[M] > T,中间值右侧的其它元素都大于 T,无需比较,中间索引左边去找,M - 1 设置为右边界,重新查找
r = m - 1;
} else {
// ③ A[M] < T,中间值左侧的其它元素都小于 T,无需比较,中间索引右边去找, M + 1 设置为左边界,重新查找
l = m + 1;
}
}
// 5. 当 L > R 时,表示没有找到,应结束循环
return -1;
}
}
public class IntegerOverFlow {
public static void main(String[] args) {
int l = 0;
int r = Integer.MAX_VALUE - 1;
int m = (r - l) / 2;
System.out.println(m); // 1073741823
// 整数溢出
int ovNum1 = overFlow1(l, r);
System.out.println(ovNum1); // -536870913
// 方法一
int ovNum2 = overFlow2(l, r);
System.out.println(ovNum2); // 536870911
// 方法二
int ovNum3 = overFlow3(l, r);
System.out.println(ovNum3); // 1073741823
}
// 无符号移位运算 整体右移,负号位变成正号位,正常计算
private static int overFlow3(int l, int r) {
int m = (l + r) >>> 1;
return (l + r) >>> 1;
}
// 避免超过 int值最大范围 2147483647
private static int overFlow2(int l, int r) {
int m = (r - l) / 2;
l = m + 1;
// (r + l) / 2 == l/2 + r/2 == l - l/2 + r/2 == l + (r - l) / 2
return l + (r + l) / 2;
}
// 整数溢出
private static int overFlow1(int l, int r) {
int m = (r - l) / 2;
l = m + 1;
return (l + r) / 2;
// 根据二进制,超过返回 int值最大范围 2147483647 时, 第一位是0, 即是负数
}
}
奇数二分取中间,偶数二分取中间靠左
1. 有一个有序表为 1,5,8,11,19,22,31,35,40,45,48,49,50 当二分查找值为 48 的结点时,查找成功需要比较的次数
答案:4
2. 使用二分法在序列 1,4,6,7,15,33,39,50,64,78,75,81,89,96 中查找元素 81 时,需要经过( )次比较
答案:4
n = l o g 2 N = l o g 10 N / l o g 10 2 n = log_2N = log_{10}N/log_{10}2 n=log2N=log10N/log102
a x = N 、 x = l o g a N a^x=N 、 x=log_{a}N ax=N、x=logaN
其中 n 为查找次数,N 为元素个数
- 是整数,则该整数即为最终结果
- 是小数,则舍去小数部分,整数加一为最终结果
3. 在拥有128个元素的数组中二分查找一个数,需要比较的次数最多不超过多少次
答案:7
冒泡排序的思想就是在每次遍历一遍未排序的数列之后,将一个数据元素浮上去(也就是排好了一个数据)
优化: 每轮冒泡时,最后一次交换索引可以作为下一轮冒泡的比较次数,如果这个值为零,表示整个数组有序,直接退出外层循环即可
public class BubbleSort {
public static void main(String[] args) {
int[] array = {3, 2, 8, 1, 9, 5, 7, 4, 6};
// 1. 循环数组
for (int i = 0; i < array.length - 1; i++) {
// 判断循环是否发生了交换
Boolean swapped = false;
// 2. 对比相邻元素(每循环一次,减少最后一位元素的循环)
for (int j = 0; j < array.length - 1 - i; j++) {
// 3. 如果左侧元素大于右边元素,换位
if(array[j] > array[j + 1]) {
int lim = array[j];
array[j] = array[j + 1];
array[j + 1] = lim;
swapped = true;
}
}
System.out.println("第" + i + "轮冒泡: " + Arrays.toString(array));
// 如果没发生交换,直接终止
if(!swapped) break;
}
System.out.println("----");
// 优化后
int[] array2 = {3, 2, 8, 1, 9, 5, 7, 4, 6};
while (true) {
int n = array2.length - 1;
int last = 0; // 最后一次交换的索引位置
for (int i = 0; i < n; i++) {
if (array2[i] > array2[i + 1]) {
int lim = array2[i];
array2[i] = array2[i + 1];
array2[i + 1] = lim;
last = i;
}
}
System.out.println("冒泡排序: " + Arrays.toString(array2));
n = last;
if(n == 0) break;
}
}
}
首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置,然后再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾,以此类推,直到所有元素均排序完毕
优化点: 为减少交换次数,每一轮可以先找最小的索引,在每轮最后再交换元素
public class SelectionSort {
public static void main(String[] args) {
int[] arr = {3, 2, 8, 1, 9, 5, 7, 4, 6};
// int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 1. 循环数组
for (int i = 0; i < arr.length - 1; i++) {
// i 代表每轮选择的最小元素交换到的目标索引
int s = i; // 代表最小元素的索引
// 2. 循环数组的元素(每次循环过滤掉首位已排序的元素)
for (int j = s + 1; j < arr.length; j++) {
// 3. 如果 选择元素 大于 目标元素,则交换索引值
if(arr[s] > arr[j]) {
s = j;
}
}
// 4. 循环后,如果最小元素的索引值发生变化,则调换两个元素位置
if(s != i) {
int lim = arr[s];
arr[s] = arr[i];
arr[i] = lim;
}
System.out.println(Arrays.toString(arr));
}
}
}
不稳定排序示例:
扑克牌先按照花色排序(♠♥♣♦),再按照数字排序(AKQJ…)
[[♠7], [♠2], [♠4], [♠5], [♥2], [♥5]]
[[♠7], [♠5], [♥5], [♠4], [♥2], [♠2]]
[[♠7], [♠2], [♠4], [♠5], [♥2], [♥5]]
[[♠7], [♠5], [♥5], [♠4], [♠2], [♥2]]
将数据按照一定的顺序一个一个的插入到有序的表中,最终得到的序列就是已经排序好的数据
优化: 插入时可以直接移动元素,而不是交换元素
public class InsertSort {
// 插入排序
public static void main(String[] args) {
int[] arr = {3, 2, 8, 1, 9, 5, 7, 4, 6};
// 1. 循环数组
for (int i = 1; i < arr.length; i++) {
int lim = arr[i]; // 待插入元素的值
int j = i - 1; // 已排序区域索引
// 2. 循环未排序的数组
while (j >= 0) {
// 3. 相邻元素比较,如果右边元素小于左边,则交换位置
if(lim < arr[j]) {
arr[j + 1] = arr[j];
} else {
break;
}
j--;
}
// 4. 将元素插入排序区域
arr[j + 1] = lim;
System.out.println("插入排序" + Arrays.toString(arr));
}
}
}
插入排序通常被同学们所轻视,其实它的地位非常重要。小数据量排序,都会优先选择插入排序
9, 18, 19, 23, 23,15
9, 15, 18, 23, 19, 23
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);
}
}
}
public class QuickSort {
// 快速排序 - 单边循环法
public static void main(String[] args) {
int[] arr = {5, 3, 7, 2, 9, 8, 1, 4};
partition(arr, 0, arr.length - 1);
quick(arr, 0, arr.length - 1);
}
// 递归
public static void quick(int[] arr, int l, int h) {
if(l >= h) return;
int p = partition(arr, l, h); // p 为索引值
quick(arr, l, p - 1); // 左边分区的范围确定
quick(arr, p + 1, h); // 右边分区的范围确定
}
private static int partition(int[] arr, int l, int h) {
int i = l; // 基准点索引
// 1. 循环数组
for (int j = l; j < h; j++) {
// 2. 循环判断元素 是否小于 索引点元素, 如果小于, 交换元素位置
if(arr[j] < arr[h]) {
if(i != j) {
int lim = arr[i];
arr[i] = arr[j];
arr[j] = lim;
}
i++;
}
}
// 3. 循环后, 将索引元素 和 基准点元素 交换位置
if(h != i) {
int pvValue = arr[h];
arr[h] = arr[i];
arr[i] = pvValue;
}
System.out.println(Arrays.toString(arr) + "i = " + i);
// 4. 返回基准点元素所在的索引,确定下一轮分区的边界
return i;
}
}
注意:
- 基准点在左边,并且要先 j 后 i
- while( i < j && a[j] > pv ) j–
- while ( i < j && a[i] <= pv ) i++
public class QuickSort2 {
// 快速排序 - 双边循环法
public static void main(String[] args) {
int[] arr = {5, 3, 7, 2, 9, 8, 1, 4};
partition(arr, 0, arr.length - 1);
quick(arr, 0, arr.length - 1);
}
private static int quick(int[] arr, int l, int h) {
return 0;
}
private static int partition(int[] arr, int l, int h) {
// 1. 选择最左边元素为基准点元素
int i = l; // 左边索引
int j = h; // 右边索引
// 2. 双边循环, 直至i、j指针重叠
while (i < j) {
// 3. j 指针负责由右向左找比基准点小的元素
while (i < j && arr[j] > arr[l]) j--;
// 4. i 指针负责由左向右找比基准点大的元素
// 4.1 <= 的目的是 避免和基准点元素交换
// 4.2 i < j 的目的是 内存循环遇到 i指针 j指针 重叠时终止
while (i < j && arr[i] <= arr[l]) i++;
// 5. 一旦找到, 二者交换位置
int lim = arr[i];
arr[i] = arr[j];
arr[j] = lim;
}
// 6. 将分区索引元素 和 基准点元素 交换位置
int lim = arr[l];
arr[l] = arr[j];
arr[j] = lim;
System.out.println(Arrays.toString(arr) + "j =" + j);
// 7. 返回基准点元素所在的索引,确定下一轮分区的边界
return j;
}
}
双边循环移动次数平均来讲比单边循环少3倍