前言
模块化编程 不仅仅是为了把算法记录下来,更重要的是以后使用起来更方便,更细致的了解他的实现。我们通过构造许多静态方法库(模块),一个库中的静态方法也能够调用到另一个库中定义的静态方法。这能够带来许多好处:
- 程序的整体代码量很大时,每次处理的模块大小仍然适中;
- 可以重用和共享代码而无需重新实现;
- 很容易用改进的实现替换老的实现;
- 可以为解决编程问题建立合适的抽象模型;
- 缩小调试范围;
单元测试
Java编程最佳实践之一,就是每个静态方法库都包含一个main()函数,测试库中的方法,随着模块的成熟,我们可以把main()方法作为一个开发用例,测试更多细节;也可以编写成测试用例对所有代码进行全面测试。当用例越来越复杂就可将它独立成一个模块。
编写算法过程(算法设计以及面试中都可以用到)
- 编写用例,在实现中把计算过程分解成可控的部分
- 明确静态方法库与之对应的API
- 实现API和能够对方法进行独立测试的main()函数
备注:虽然整体上都是使用基本类型测试的但是以上思想还是很重要的,在基本类型的基础工具类中任然可以将一些不变的代码封装起来。简化,解耦代码。
二分搜索(又叫二分查找、折半搜索)
两个基本的类:
BasicTool 放置算法需要的方法以供调用;
BasicAlgorithm 放置基本算法;
两个类中各有一个main()函数作为测试方法,测试方法的结果输出是否正确。
通用代码:
public class BasicTool {
//比较两个元素大小
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
//交换元素位置 包装类
private static void exch(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
//针对int基本类型的交换
public static void swap(int[] arr, int one, int two) {
int temp = arr[one];
arr[one] = arr[two];
arr[two] = temp;
}
public static boolean isSorted(Comparable[] a) { // 测试数组元素是否有序
for (int i = 1; i < a.length; i++)
if (less(a[i], a[i - 1])) return false;
return true;
}
}
二分搜索:
public class BasicAlgorithm {
public static int binarySearch(int a[], int target) {
//数组必须有序
int low = 0;
int high = a.length - 1;
while (low <= high) {
//被查找的元素要不存在,要么必然存在于a[low....high]
int mid = low + (high - low) / 2;
if (target < a[mid]) {
high = mid - 1;
} else if (target > a[mid]) {
low = mid + 1;
} else {
return mid;
}
}
return -1;
}
public static void main(String[] args) {
int[] a = {1, 2, 5, 5, 6, 8, 9};
System.out.println(binarySearch(a, 6) + "");
}
}
扩展:
二分搜索的递归(Recursion)实现:
public static int binarySearchRecursion(int a[], int target, int low, int high) {
if (low > high) return -1;
//被查找的元素要不存在,要么必然存在于a[low....high]
int mid = low + (high - low) / 2;
if (target < a[mid])
return binarySearchRecursion(a, target, low, mid - 1);
else if (target > a[mid])
return binarySearchRecursion(a, target, mid + 1, high);
else return mid;
}
public static void main(String[] args) {
int[] a = {1, 2, 5, 5, 6, 8, 9};
System.out.println(binarySearchRecursion(a, 6, 0, a.length - 1) + "");
}
在扩展下:
递归要注意啥,有啥好处?
更加简洁易懂。代码少了其实需要我们写递归的时候注意的也要多三点:
- 递归总有一个最简单的情况——方法的第一条总是一个包含return的条件语句
if (low > high) return -1;
- 递归调用总是尝试去解决一个规模更小的子问题,这样才能收敛到最简单的情况,就像low和high的差值越来越小,搜索的区间(规模)越来越小
- 递归调用的父问题和尝试解决的子问题之间不能有交集,虽然low和high的差值越来越小,但是父问题和子问题操作的区间是不同的,也就是他们操作的数组部分是不同的
排序算法
扩展:
Java API自带排序方法:Arrays.sort() 他的排序大体上对于Java基本类型使用的是快速排序,对对象数组使用的是归并排序,但是Java8对于排序进一步优化,根据不同类型和剩余需要排序元素的个数,来细化使用排序的类型(比如插入排序等)。
原因:使用不同类型的排序算法主要是由于快速排序是不稳定的,而合并排序是稳定的。这里的稳定是指比较相等的数据在排序之后仍然按照排序之前的前后顺序排列。对于基本数据类型,稳定性没有意义,而对于对象类型,稳定性是比较重要的,因为对象相等的判断可能只是判断关键属性,最好保持相等对象的非关键属性的顺序与排序前一致。
另外一个原因是由于合并排序相对而言 比较次数 比快速排序少, 移动(对象引用的移动)次数 比快速排序多,而对于对象来说,移动是简单的,只是引用的转换,但是比较相对更加耗时。
合并排序的时间复杂度是nlogn, 快速排序的平均时间复杂度也是nlogn,但是合并排序的需要额外的n个引用的空间。
- 对于List列表类的排序使用:Collections.sort() 两者有异曲同之妙。
- 你需要知道的在这里:参考链接
冒泡排序
它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。每趟比较交换完之后,最后的元素是最大的数,所以每趟结束后参与排序的元素个数n-1-i。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
时间复杂度
o(n^2)
注释部分为优化部分。
// 冒泡:n-1 n-1-i
// 冒泡排序:两两比较,比较n-1趟,每一趟比较n-i-1次
public static int[] bubbleSort(int[] a) {
int n = a.length;
for (int i = 0; i < n - 1; i++) {
//boolean isSwap = false;
for (int j = 0; j < n - 1 - i; j++) {
if (a[j] > a[j + 1]) {
a[j + 1] = a[j] ^ a[j + 1];
a[j] = a[j] ^ a[j + 1];
a[j + 1] = a[j] ^ a[j + 1];
isSwap = true;
}
}
//if (!isSwap) {
// return a;
//}
}
return a;
}
选择排序
时间复杂度
o(n^2)
首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此类推,直到所有元素均排序完毕。
//选择排序:遍历n-1趟元素,设置i为最小数下标即哨兵,与 i+1 直到 n 个元素比较,记录小数下标,如果最终记录的min和开始设置的哨兵min(i)不一致,与下标为i的元素交换位置
public static int[] selectionSort(int[] a) {
int n = a.length;
for (int i = 0; i < n - 1; i++) {
int min = i;
for (int j = i + 1; j < n; j++) {
if (a[min] > a[j]) {
min = j;
}
}
if (min != i) {
swap(a, min, i);
}
}
return a;
}
插入排序
时间复杂度
o(n^2)
它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
//插入排序:左边为有序区域,遍历第一个直到第n个,每次加入一个新的元素插入到前边有序的元素中比较j和j-1元素如果a[j-1] > a[j]大小交换位置else测跳出这层循环
public static int[] insertionSort(int[] a) {
int n = a.length;
for (int i = 1;i<n;i++) {
for (int j = i;j>0;j--) {
if (a[j] < a[j-1]) {
swap(a,j,j-1);
} else {
break;
}
}
}
return a;
}
归并排序
时间复杂度
O(n*log
n)
需要额外空间,针对两个有序数组
该算法是采用分治法(Divide and Conquer)的一个非常典型的应用
申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
设定两个指针,最初位置分别为两个已经排序序列的起始位置;
比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
重复步骤3直到某一指针达到序列尾;
将另一序列剩下的所有元素直接复制到合并序列尾
//归并排序 递归方式
public static void mergeSort(int[] oldArr, int left, int right) {
if (left >= right)//保证可分的数组至少有两个元素 一个元素是不可分的
return;
//去一个中间数
int center = left + (right - left) / 2;//(right + left)/2
//对左边数组递归
mergeSort(oldArr, left, center);
//对右边数组递归
mergeSort(oldArr, center + 1, right);
//合并
merge(oldArr, left, center, right);
}
public static void merge(int[] oldArr, int left, int center, int right) {
print(oldArr, left, center, right);
//临时新的数组
int[] newArr = new int[right - left + 1];
//左边第一个元素
int i = left;
//右边数组第一个元素
int j = center + 1;
//新数组指针
int k = 0;
//两个指针指向的元素比较 谁的比较小就放到新数组里 指针加1
while (i <= center && j <= right) {
if (oldArr[i] < oldArr[j])
newArr[k++] = oldArr[i++];
else
newArr[k++] = oldArr[j++];
}
//两个While只有一个可以运行 最后只有一个数组会剩余元素 把剩余元素移入数组,谁没指完谁剩余,谁放入数组
while (i <= center) {
newArr[k++] = oldArr[i++];
}
while (j <= right) {
newArr[k++] = oldArr[j++];
}
//然后把新数组排好序的数覆盖就数组中对应得数
for (int kk = 0; kk < newArr.length; kk++) {
oldArr[kk + left] = newArr[kk];
}
print(oldArr);
}
public static void print(int[] data, int left, int center, int right) {
System.out.println();
System.out.println("leftNum:" + left);
System.out.println("centerNum:" + center);
System.out.println("rightNum:" + right);
System.out.println("left:");
for (int i = left; i <= center; i++) {
System.out.print(data[i] + "\t");
}
System.out.println();
System.out.println("right:");
for (int i = center + 1; i <= right; i++) {
System.out.print(data[i] + "\t");
}
System.out.println();
}
public static void print(int[] data) {
System.out.println();
for (int i = 0; i < data.length; i++) {
System.out.print(data[i] + "\t");
}
System.out.println();
}
public static void main(String[] args) {
int[] a = {1, 24, 54, 7, 2, 8, 23, 45, 1, 2, 3};
mergeSort(a, 0, a.length - 1);
for (int i : a) {
System.out.print(i + " ");
}
}
合并流程打印结果
leftNum:0
centerNum:0
rightNum:1
left:
1
right:
24
1 24 54 7 2 8 23 45 1 2 3
leftNum:0
centerNum:1
rightNum:2
left:
1 24
right:
54
1 24 54 7 2 8 23 45 1 2 3
leftNum:3
centerNum:3
rightNum:4
left:
7
right:
2
1 24 54 2 7 8 23 45 1 2 3
leftNum:3
centerNum:4
rightNum:5
left:
2 7
right:
8
1 24 54 2 7 8 23 45 1 2 3
leftNum:0
centerNum:2
rightNum:5
left:
1 24 54
right:
2 7 8
1 2 7 8 24 54 23 45 1 2 3
leftNum:6
centerNum:6
rightNum:7
left:
23
right:
45
1 2 7 8 24 54 23 45 1 2 3
leftNum:6
centerNum:7
rightNum:8
left:
23 45
right:
1
1 2 7 8 24 54 1 23 45 2 3
leftNum:9
centerNum:9
rightNum:10
left:
2
right:
3
1 2 7 8 24 54 1 23 45 2 3
leftNum:6
centerNum:8
rightNum:10
left:
1 23 45
right:
2 3
1 2 7 8 24 54 1 2 3 23 45
leftNum:0
centerNum:5
rightNum:10
left:
1 2 7 8 24 54
right:
1 2 3 23 45
1 1 2 2 3 7 8 23 24 45 54
快速排序
最好时间复杂度:
O(n*log
n)
最坏时间复杂度(不常见):
O(n^2)
平均时间复杂度:
O(n*log
n)
事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来,且在大部分真实世界的数据,可以决定设计的选择,减少所需时间的二次方项之可能性。
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
//获取标记位 并且根据标记为元素大小来进行大于标记元素和小于标记元素的划分
public static int partition(int[] a, int low, int high) {
//低位
int i = low;
//高位
int j = high;
int privotKey = a[low];
while (i < j) {
//低位高位交替执行 交换位置 等于的情况下对基本类型的稳定性没有影响,内部某一个while循环完毕,交换时,privotKey总是在i或者是j的位置上
while (i < j && a[j] >= privotKey) --j;
swap(a, i, j);
while (i < j && a[i] <= privotKey) ++i;
swap(a, i, j);
}
return i;//返回high也可,while循环停止时,最后i == j
}
//递归除了标记元素之外的数组进行 partition
public static void quickSort(int[] a, int low, int high) {
if (low < high) {
int privotLoc = partition(a, low, high);
quickSort(a, low, privotLoc - 1);
quickSort(a, privotLoc + 1, high);
}
}
public static void main(String[] args) {
int[] a = {1, 24, 54, 7, 2, 8, 23, 45, 1, 2, 3};
quickSort(a, 0, a.length - 1);
for (int i : a) {
System.out.print(i + " ");
}
}
如果初始序列按关键码有序或基本有序时,快排序反而蜕化为冒泡排序。所以改进方法:
快速排序的改进在本改进算法中,只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。
//partition不变,更改下递归区间。
public static void quickSortPro(int[] a, int low, int high, int k) {
//长度大于k时递归, k为指定的数
if (high - low > k) {
int privotLoc = partition(a, low, high);
quickSort(a, low, privotLoc - 1);
quickSort(a, privotLoc + 1, high);
}
bubbleSort(a);
}
public static void main(String[] args) {
int[] a = {1, 24, 54, 7, 2, 8, 23, 45, 1, 2, 3, 6, 4, 78, 43, 67, 54, 32, 47};
quickSortPro(a, 0, a.length - 1, 8);
for (int i : a) {
System.out.print(i + " ");
}
}
[参考]:
《算法》第四版