算法学习
二分查找
算法描述
-
前提:有已排序数组 A(假设已经做好)
-
定义左边界 L、右边界 R,确定搜索范围,循环执⾏⼆分查找(3、4两步)
-
获取中间索引 M = Floor((L+R) /2)
-
中间索引的值 A[M] 与待搜索的值 T 进行比较
① A[M] == T 表示找到,返回中间索引
② A[M] > T,中间值右侧的其它元素都⼤于 T,⽆需⽐较,中间索引左边去找,M - 1 设置为右边界,重新查找
③ A[M] < T,中间值左侧的其它元素都⼩于 T,⽆需⽐较,中间索引右边去找, M + 1设置为左边界,重新查找
- 当 L > R 时,表示没有找到,应结束循环
算法实现
public class BinarySearch {
public static void main(String[] args) {
// 1.定义有序的int类型数组
int[] array = {24, 33, 45, 56, 58, 62, 74, 89, 102};
// 2.定义需要查找的对象
int target = 74;
// 3.调⽤⼆分查找⽅法函数 获得对象在数组中索引值
int index = binarySearch(array, target);
// 4.输出结果
System.out.println("index = " + index);
}
private static int binarySearch(int[] array, int target) {
// 1.定义左右边界和中间值
int left = 0, right = array.length - 1, m = 0;
// 2.循环执⾏⼆分查找
while (left <= right) {
// 避免int整数相加溢出 更换减法 left/2 + right/2 ==>
//left + (-left / 2 + right / 2) ==>left + (right - left) / 2
//m = (left + right) / 2;
//m = left + (right - left) / 2;
// 采⽤位运算右移1位则代表除以2
m = (left + right) >>> 1;
// 3.判断查找对象和遍历的当前值
if (array[m] == target) {
return m;
} else if (array[m] > target) {
right = m - 1;
} else {
left = m + 1;
}
}
return -1;
}
}
记得⼀个简要判断⼝诀:奇数⼆分取中间,偶数⼆分取中间靠左。
冒泡排序
算法描述
- 依次⽐较数组中相邻两个元素⼤⼩,若 a[j] > a[j+1],则交换两个元素,两两都⽐较⼀遍
称为⼀轮冒泡,结果是让最⼤的元素排⾄最后
- 重复以上步骤,直到整个数组有序
算法实现
/**
* 通过判断当前是否发⽣变化从⽽优化算法
*
* 优化点1:每经过⼀轮冒泡,内层循环就可以减少⼀次
* 优化点2:如果某⼀轮冒泡没有发⽣交换,则表示所有数据有序,可以结束外层循环
* 优化算法实现
*
* @param array
*/
private static void bubble(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean swapped = false;
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
swapped = true;
}
}
System.out.println("第" + (i + 1) + "轮数组 = " +
Arrays.toString(array));
if (!swapped) {
break;
}
}
}
-
优化点1:每经过⼀轮冒泡,内层循环就可以减少⼀次
-
优化点2:如果某⼀轮冒泡没有发⽣交换,则表示所有数据有序,可以结束外层循环
优化算法实现
/**
* 通过判断最后⼀次交换值是否发送改变
* 从⽽判断最后⼀次被交换的索引⾄最后⼀位已有序 进⽽优化算法
*
* @param array
*/
private static void bubble(int[] array) {
int swapIndex = array.length - 1;
do {
// 1.进⼊循环⾄0
int last = 0;
for (int i = 0; i < swapIndex; i++) {
if (array[i] > array[i + 1]) {
swap(array, i, i + 1);
// 2.记录最后⼀次交换值的索引
last = i;
}
}
// 3.将最后⼀次交换的索引值赋值 作为下⼀次排序遍历的⻓度
swapIndex = last;
// 4.判断当前是否已经交换完 形成有序数组
System.out.println("Arrays.toString(array) = " +
Arrays.toString(array));
} while (swapIndex != 0);
}
-
每轮冒泡时,最后⼀次交换索引可以作为下⼀轮冒泡的⽐较次数,如果这个值为零,表示
整个数组有序,直接退出外层循环即可
选择排序
算法描述
- 将数组分为两个⼦集,排序的和未排序的,每⼀轮从未排序的⼦集中选出最⼩的元素,放
⼊排序⼦集
- 重复以上步骤,直到整个数组有序
算法实现
/**
* 选择排序-算法实现
*
* @param array
*/
private static void selection(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
// i代表每轮排序最⼩值需要交换到的具体索引
// selectIndex代表每轮已被选择的值当前在数据中的具体索引
int selectIndex = i;
for (int j = selectIndex + 1; j < array.length; j++) {
// 遍历(选择)剩余未被排序的其他数组
if (array[selectIndex] > array[j]) {
// 记录当前该轮出现最⼩值
selectIndex = j;
// 当争论执⾏完再进⾏交换 减少交换次数
//swap(array, i, j);
}
}
if (selectIndex != i) {
swap(array, selectIndex, i);
}
System.out.println("Arrays.toString(array) = " +
Arrays.toString(array));
}
}
public static void main(String[] args) {
// 1.定义有序的int类型数组
int[] array = {54, 33, 45, 56, 58, 22, 64, 89, 12};
// 2.调⽤选择排序⽅法函数
selection(array);
}
与冒泡排序⽐较
-
⼆者平均时间复杂度都是 O(n^2)
-
选择排序⼀般要快于冒泡,因为其交换次数少
-
但如果集合有序度⾼,冒泡优于选择
-
冒泡属于稳定排序算法,⽽选择属于不稳定排序
- 稳定排序指,按对象中不同字段进⾏多次排序,不会打乱同值元素的顺序
不稳定排序则反之
-
选择排序和冒泡排序的时间复杂度都是O(n^2),这意味着当处理大规模数据时,它们的效率可能较低。这两种排序算法通常用于教学目的,以及处理小规模或部分已排序的数据集。对于大规模数据集,更高效的排序算法(如快速排序、归并排序等)通常是更好的选择。
插入排序
算法描述
-
将数组分为两个区域,排序区域和未排序区域,每⼀轮从未排序区域中取出第⼀个元素,插⼊到排序区域(需保证顺序)
-
重复以上步骤,直到整个数组有序
/**
* 插⼊排序-算法实现
*
* @param array
*/
private static void insertSort(int[] array) {
// 当前i表示需要进⾏插⼊的元素的索引值
for (int i = 1; i < array.length; i++) {
// 通过临时变量存储需要向前插⼊元素的值
int temp = array[i];
// 通过i值找到与之前⼀位的索引值
int front = i - 1;
while (front >= 0) {
if (temp < array[front]) {
// 若需要插⼊元素值⼩于前⼀位元素值 较⼤值后移
array[front + 1] = array[front];
} else {
// 已找到需要插⼊的位置索引值 退出循环
break;
}
front--;
}
// 向⽐较索引值的后⼀位进⾏插⼊操作
array[front + 1] = temp;
System.out.println("Arrays.toString(array) = " +
Arrays.toString(array));
}
}
public static void main(String[] args) {
// 1.定义有序的int类型数组
int[] array = {54, 33, 45, 56, 58, 22, 64, 89, 12};
// 2.调⽤
insertSort(array);
}
与选择排序⽐较
-
二者平均时间复杂度都是 O(n^2)
-
大部分情况下,插⼊都略优于选择(选择略优于冒泡)
-
有序集合插⼊的时间复杂度为 O(n)
-
插⼊属于稳定排序算法,⽽选择属于不稳定排序
警示
插⼊排序易被轻视,其实它的地位非常重要。小数据量排序,都会优先选择插⼊排序
希尔排序
算法描述
-
首先选取⼀个间隙序列,如 (n/2,n/4 … 1),n 为数组⻓度
-
每⼀轮将间隙相等的元素视为⼀组,对组内元素进⾏插⼊排序,⽬的有⼆
① 少量元素插⼊排序速度很快
② 让组内值较⼤的元素更快地移动到后⽅
- 当间隙逐渐减少,直⾄为 1 时,即可完成排序
private static void shellSort(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 static void shellSort(int[] array) {
int n = array.length;
int gap = n / 2; // 初始间隔为数组长度的一半
// 逐步缩小间隔
while (gap > 0) {
// 对每个子序列进行插入排序
for (int i = gap; i < n; i++) {
int temp = array[i];
int j;
// j-gap 是上⼀个元素索引,如果 > t,后移
for (j = i; j >= gap && array[j - gap] > temp; j -= gap) {
array[j] = array[j - gap];
}
array[j] = temp;
}
gap /= 2; // 缩小间隔
}
}
public static void main(String[] args) {
int[] array = {9, 8, 3, 7, 5, 1, 4, 6, 2, 0};
shellSort(array);
for (int i : array) {
System.out.print(i + " ");
}
}
希尔排序是对插入排序的一种改进,在效率上较直接插入、冒泡、选择排序方法有较大改进。它适用于中等规模的数据排序,是冲破O(n^2)的第一批算法之一
快速排序
算法描述
- 每⼀轮排序选择⼀个基准点(pivot)进⾏分区
-
让⼩于基准点的元素的进⼊⼀个分区,⼤于基准点的元素的进⼊另⼀个分区
-
当分区完成时,基准点元素的位置就是其最终位置
-
在⼦分区内重复以上过程,直⾄⼦分区元素个数少于等于 1,这体现的是分⽽治之的思想(divide-and-conquer)
-
从以上描述可以看出,⼀个关键在于分区算法,常⻅的有洛穆托分区⽅案、双边循环分区方案、霍尔分区⽅案
public static void main(String[] args) {
int[] array = {9, 8, 3, 7, 5, 1, 4, 6, 2, 0};
quickSort(array, 0, array.length - 1);
for (int i : array) {
System.out.print(i + " ");
}
}
public static void quickSort(int[] array, int low, int high) {
if (low < high) {
int pivotIndex = partition(array, low, high);
quickSort(array, low, pivotIndex - 1);
quickSort(array, pivotIndex + 1, high);
}
}
public static int partition(int[] array, int low, int high) {
int pivot = array[high]; // 选择最后一个元素作为基准
int i = (low - 1); // 小于基准的元素索引
for (int j = low; j <= high - 1; j++) {
if (array[j] < pivot) {
i++;
swap(array, i, j);
}
}
swap(array, i + 1, high); // 将基准元素放到正确的位置
return i + 1;
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
它的基本思想是:通过一次排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序的算法实现通常包括一个基准元素(pivot)的选择和分区(partitioning)过程