二分查找
1、前提:有已排序数组 A(假设已经做好)
2、定义左边界 L、右边界 R,确定搜索范围,循环执行二分查找(3、4两步)
3、获取中间索引 M= Floor((L+R)/2)
4、中间索引的值A[M]与待搜索的值Ⅰ进行比较
A[M] == T表示找到,返回中间索引
A(M] > T,中间值右侧的其它元素都大于T,无需比较,中间索引左边去找,M - 1 设置为右边界,重新查找
A[M] < T,中间值左侧的其它元素都小于T,无需比较,中间索引右边去找,M + 1 设置为左边界,重新查找
5、当 L > R 时,表示没有找到,应结束循环
代码实现
public class BinarySearch {
public static void main(String[] args) {
int[] a = {1, 5, 8, 11, 19, 22, 31, 35, 40, 45, 48, 49, 50};
int target = 48;
System.out.println(binarySearch(a, target));
}
//找到返回元素下标,找不到返回-1
private static int binarySearch(int[] a, int target) {
int l = 0;
int r = a.length - 1;
int m;
while (l <= r) {
m = (l + r) / 2;
if (a[m] < target) {
l = m + 1;
} else if (a[m] > target) {
r = m - 1;
} else {
return m;
}
}
return -1;
}
}
发现的问题
这里计算中间值m的时候用的是:m = (l + r) / 2; 如果此时r为int能容纳的最大数,则此时会发生整数溢出,解决方法可以是变换一下算式,变为 m = (l + r) / 2; 也可以进行位运算,正整数向右移一位相当于除二
冒泡排序
给出一组数组{5, 9, 7, 4, 1, 3, 2, 8},对其进行冒泡排序
初步实现
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] a = {5, 9, 7, 4, 1, 3, 2, 8};
//int [] a = {1, 2, 3, 4, 5, 7, 8, 9};
bubble(a);
}
private static void bubble(int[] a) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = 0; j < a.length - 1; j++) {
System.out.print("比较" + (j + 1) + "次 ");
if (a[j] > a[j + 1]) {
Method.swap(a, j, j + 1);
}
}
System.out.println("第" + (i + 1) + "轮冒泡排序,数组变成:" + Arrays.toString(a));
}
}
}
其中: Method.swap(a, j, j + 1); 为自己封装的类中的方法, 用来进行数组值的交换
public class Method {
public static void swap(int[] a, int i, int j){
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
执行结果:
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 比较7次 第1轮冒泡排序,数组变成:[5, 7, 4, 1, 3, 2, 8, 9]
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 比较7次 第2轮冒泡排序,数组变成:[5, 4, 1, 3, 2, 7, 8, 9]
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 比较7次 第3轮冒泡排序,数组变成:[4, 1, 3, 2, 5, 7, 8, 9]
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 比较7次 第4轮冒泡排序,数组变成:[1, 3, 2, 4, 5, 7, 8, 9]
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 比较7次 第5轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 比较7次 第6轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 比较7次 第7轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
发现的问题
1、从执行结果中可以看到,在冒泡排序的过程,已经排好序的数组下标仍会进行比较。
2、从执行结果中可以看到,自从第六轮冒泡比较完1和2之后,数组已经有序,不需要再进行冒泡排序了。
优化
1、减少比较次数
每轮冒泡排序完,最大的数都会交换到数组的末尾,下一次冒泡则不需要与其进行比较。
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] a = {5, 9, 7, 4, 1, 3, 2, 8};
//int [] a = {1, 2, 3, 4, 5, 7, 8, 9};
bubble(a);
}
private static void bubble(int[] a) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = 0; j < a.length - 1 - i; j++) {
System.out.print("比较" + (j + 1) + "次 ");
if (a[j] > a[j + 1]) {
Method.swap(a, j, j + 1);
}
}
System.out.println("第" + (i + 1) + "轮冒泡排序,数组变成:" + Arrays.toString(a));
}
}
}
执行结果:
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 比较7次 第1轮冒泡排序,数组变成:[5, 7, 4, 1, 3, 2, 8, 9]
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 第2轮冒泡排序,数组变成:[5, 4, 1, 3, 2, 7, 8, 9]
比较1次 比较2次 比较3次 比较4次 比较5次 第3轮冒泡排序,数组变成:[4, 1, 3, 2, 5, 7, 8, 9]
比较1次 比较2次 比较3次 比较4次 第4轮冒泡排序,数组变成:[1, 3, 2, 4, 5, 7, 8, 9]
比较1次 比较2次 比较3次 第5轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
比较1次 比较2次 第6轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
比较1次 第7轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
2、减少冒泡次数
如果数组中比较之后没有需要交换的下标,则说明数组已经有序,则不需要在进行冒泡排序。
因此我们可以将是否交换进行一个标记,如果没有交换,则退出for循环。
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] a = {5, 9, 7, 4, 1, 3, 2, 8};
//int [] a = {1, 2, 3, 4, 5, 7, 8, 9};
bubble(a);
}
private static void bubble(int[] a) {
for (int i = 0; i < a.length - 1; i++) {
Boolean flag = false;//是否进行交换
for (int j = 0; j < a.length - 1 - i; j++) {
System.out.print("比较" + (j + 1) + "次 ");
if (a[j] > a[j + 1]) {
flag = true;
Method.swap(a, j, j + 1);
}
}
System.out.println("第" + (i + 1) + "轮冒泡排序,数组变成:" + Arrays.toString(a));
if (!flag) {
break;
}
}
}
}
执行结果:
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 比较7次 第1轮冒泡排序,数组变成:[5, 7, 4, 1, 3, 2, 8, 9]
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 第2轮冒泡排序,数组变成:[5, 4, 1, 3, 2, 7, 8, 9]
比较1次 比较2次 比较3次 比较4次 比较5次 第3轮冒泡排序,数组变成:[4, 1, 3, 2, 5, 7, 8, 9]
比较1次 比较2次 比较3次 比较4次 第4轮冒泡排序,数组变成:[1, 3, 2, 4, 5, 7, 8, 9]
比较1次 比较2次 比较3次 第5轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
比较1次 比较2次 第6轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
最终实现
通过上面的执行结果,我们可以得知:每轮的比较次数都是通过 i 来控制的,但是实际上还是会存在重复比较。就举一个上面的例子:
比较1次 比较2次 比较3次 第5轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
比较1次 比较2次 第6轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
在这里第5轮冒泡已经比较了 a[1] 和 a[2] ,此时第6轮冒泡再次比较两次就算是重复比较了,因此我们可以选择一个变量接收上一轮最后一次交换的数组下标,从而控制下一轮的比较次数。
import java.util.Arrays;
public class BubbleSort {
public static void main(String[] args) {
int[] a = {5, 9, 7, 4, 1, 3, 2, 8};
bubble2(a);
}
private static void bubble2(int[] a) {
int n = a.length - 1;
int i = 1;//表示排序次数
while (true) {
int last = 0;//表示最后一次交换索引下标
for (int j = 0; j < n; j++) {
System.out.print("比较" + (j + 1) + "次 ");
if (a[j] > a[j + 1]) {
last = j;
Method.swap(a, j, j + 1);
}
}
n = last;
System.out.println("第" + i + "轮冒泡排序,数组变成:" + Arrays.toString(a));
i++;
if (n == 0) {
break;
}
}
}
}
执行结果:
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 比较7次 第1轮冒泡排序,数组变成:[5, 7, 4, 1, 3, 2, 8, 9]
比较1次 比较2次 比较3次 比较4次 比较5次 比较6次 第2轮冒泡排序,数组变成:[5, 4, 1, 3, 2, 7, 8, 9]
比较1次 比较2次 比较3次 比较4次 第3轮冒泡排序,数组变成:[4, 1, 3, 2, 5, 7, 8, 9]
比较1次 比较2次 比较3次 第4轮冒泡排序,数组变成:[1, 3, 2, 4, 5, 7, 8, 9]
比较1次 比较2次 第5轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
比较1次 第6轮冒泡排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
比较次数再一次减少。
选择排序
1、将数组分为两个子集,排序的和未排序的,每一轮从未排序的子集中选出最小的元素,放入排序子集。
2、重复以上步骤.直到整个数组有序。
给出一组数组{5, 3, 7, 2, 1, 9, 8, 4},对其进行选择排序。
import java.util.Arrays;
public class SelectionSort {
public static void main(String[] args) {
int[] a = {5, 3, 7, 2, 1, 9, 8, 4};
selection(a);
}
private static void selection(int[] a) {
for (int i = 0; i < a.length - 1; i++) {
//m代表每轮选择最小元素要交换的下标
int m = i;
for (int j = m + 1; j < a.length; j++) {
if (a[m] > a[j]) {
m = j;
}
}
if (m != i){
Method.swap(a,m,i);
}
System.out.println("第" + (i + 1) + "轮选择排序,数组变成:" + Arrays.toString(a));
}
}
}
执行结果:
第1轮选择排序,数组变成:[1, 3, 7, 2, 5, 9, 8, 4]
第2轮选择排序,数组变成:[1, 2, 7, 3, 5, 9, 8, 4]
第3轮选择排序,数组变成:[1, 2, 3, 7, 5, 9, 8, 4]
第4轮选择排序,数组变成:[1, 2, 3, 4, 5, 9, 8, 7]
第5轮选择排序,数组变成:[1, 2, 3, 4, 5, 9, 8, 7]
第6轮选择排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
第7轮选择排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
冒泡排序和选择排序比较
1、二者平均时间复杂度都是O(n^2)。
2、选择排序一般要快于冒泡,因为其交换次数少。
3、但如果集合有序度高,冒泡优于选择。
4、冒泡属于稳定排序算法,而选择属于不稳定排序。
这里的稳定不稳定是根据相同数据会不会颠倒来决定的。
插入排序
1、将数组分为两个区域,排序区域和未排序区域,每一轮从未排序区域中取出第一个元素,插入到排序区域(需保证顺序)。
2、重复以上步骤,直到整个数组有序。
给出一组数组{5, 3, 7, 2, 1, 9, 8, 4},对其进行插入排序。
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args) {
int[] a = {5, 3, 7, 2, 1, 9, 8, 4};
insert(a);
}
private static void insert(int[] a) {
//i表示待插入的索引
for (int i = 1; i < a.length; i++) {
//m表示待插入的值
int m = a[i];
//j表示已排序的索引
int j = i - 1;
while (j >= 0) {
if (a[j] > m) {
a[j + 1] = a[j];
} else {
break;
}
j--;
}
a[j + 1] = m;
System.out.println("第" + i + "轮插入排序,数组变成:" + Arrays.toString(a));
}
}
}
执行结果:
第1轮插入排序,数组变成:[3, 5, 7, 2, 1, 9, 8, 4]
第2轮插入排序,数组变成:[3, 5, 7, 2, 1, 9, 8, 4]
第3轮插入排序,数组变成:[2, 3, 5, 7, 1, 9, 8, 4]
第4轮插入排序,数组变成:[1, 2, 3, 5, 7, 9, 8, 4]
第5轮插入排序,数组变成:[1, 2, 3, 5, 7, 9, 8, 4]
第6轮插入排序,数组变成:[1, 2, 3, 5, 7, 8, 9, 4]
第7轮插入排序,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
插入排序的缺点:
如果较大的元素在数组前面,则会进行多次的移动。
插入排序和选择排序比较
1、二者平均时间复杂度都是O(n^2)。
2、大部分情况下,插入都略优于选择。
3、有序集合插入的时间复杂度为O(n)。
4、插入属于稳定排序算法,而选择属于不稳定排序。
希尔排序
希尔排序是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。它是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
快速排序
1、每一轮排序选择一个基准点 (pivot) 进行分区。
1.让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区。
2.当分区完成时,基准点元素的位置就是其最终位置。
2、在子分区内重复以上过程,直至子分区元素个数少于等于1,这体现的是分而治之的思想。
实现方式
快排的实现方式可谓五花八门,其中最经典的快排有三种:单边循环快排(洛穆托分区)、双边循环快排、霍尔分区
单边循环快排(洛穆托分区)
1、选择最右元素作为基准点元素。
2、j 指针负责找到比基准点小的元素,一旦找到则与 i 进行交换。
3、i 指针维护小于基准点元素的边界,也是每次交换的目标索引。
4、最后基准点与 i 交换,i 即为分区位置。
给出一组数组{5, 3, 7, 2, 9, 8, 1, 4},对其进行单边循环快排
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args) {
int[] a = {5, 3, 7, 2, 9, 8, 1, 4};
recursion(a, 0, a.length - 1);
}
//递归
private static void recursion(int[] a, int l, int h) {
if (l >= h) {
return;
}
int i = quick(a, l, h);
recursion(a, l, i - 1);
recursion(a, i + 1, h);
}
private static int quick(int[] a, int l, int h) {
//基准点元素
int pv = a[h];
int i = l;
for (int j = l; j < h; j++) {
if (a[j] < pv) {
Method.swap(a, i, j);
i++;
}
}
//循环结束之后,将基准点和i调换
Method.swap(a, h, i);
System.out.println("单边循环快排,数组变成:" + Arrays.toString(a));
return i;
}
}
执行结果:
单边循环快排,数组变成:[3, 2, 1, 4, 9, 8, 7, 5]
单边循环快排,数组变成:[1, 2, 3, 4, 9, 8, 7, 5]
单边循环快排,数组变成:[1, 2, 3, 4, 9, 8, 7, 5]
单边循环快排,数组变成:[1, 2, 3, 4, 5, 8, 7, 9]
单边循环快排,数组变成:[1, 2, 3, 4, 5, 8, 7, 9]
单边循环快排,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
双边循环快排
1、选择最左元素作为基准点元素。
2、j 指针负责从右向左找比基准点小的元素,i 指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至 i,j 相交。
3、最后基准点与 i (此时 i 与 j 相等)交换,i 即为分区位置。
给出一组数组{5, 3, 7, 2, 9, 8, 1, 4},对其进行双边循环快排
import java.util.Arrays;
public class QuickSort2 {
public static void main(String[] args) {
int[] a = {5, 3, 7, 2, 9, 8, 1, 4};
recursion(a, 0, a.length - 1);
}
private static void recursion(int[] a, int l, int h) {
if (l >= h) {
return;
}
int i = quick(a, l, h);
recursion(a, l, i - 1);
recursion(a, i + 1, h);
}
private static int quick(int[] a, int l, int h) {
int pv = a[l];
int i = l;
int j = h;
while (i < j) {
//j向左找比基准点小的
while (i < j && a[j] > pv) {
j--;
}
//i向右找比基准点大的
while (i < j && a[i] <= pv) {
i++;
}
//找到二者互换
Method.swap(a, i, j);
}
//最后将基准点和i互换
Method.swap(a, l, i);
System.out.println("双边循环快排,数组变成:" + Arrays.toString(a));
return i;
}
}
执行结果:
双边循环快排,数组变成:[1, 3, 4, 2, 5, 8, 9, 7]
双边循环快排,数组变成:[1, 3, 4, 2, 5, 8, 9, 7]
双边循环快排,数组变成:[1, 2, 3, 4, 5, 8, 9, 7]
双边循环快排,数组变成:[1, 2, 3, 4, 5, 7, 8, 9]
快速排序的特点
1、平均时间复杂度是O(n log n),最坏时间复杂度O(n^2)。
2、数据量较大时,优势非常明显。
3、属于不稳定排序。