递归
介绍
简单的说:递归就是方法自己刁颖自己,每次调用传入不同的变量,递归有助于编程者解觉复杂的问题,同时可以让代码变得简洁
递归应用场景
- 数学问题:迷宫问题、8皇后问题、汉诺塔、阶乘问题、球和篮子问题等等
- 算法问题:快排、归并排序、二分查找、分治算法等
递归调用机制
打印问题 ```java public class TestMain {
public static void main(String[] args) {
test(4);
}
public static void test(int n) {
if (n > 2) {test(n-1);}System.out.println("n = " + n);
}
}
2. 阶乘问题```javapublic class TestMain {public static void main(String[] args) {System.out.println(test1(5));}public static int test1(int n) {if (n == 1) {return 1;}return test1(n - 1) * n;}}
递归需要遵守的重要规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响
- 如果方法中使用的是引用类型变量,就会共享该引用类型的数据
- 递归必须向退出递归条件逼近,否则就是无限递归(StackOverflowError)
- 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法就执行完毕
迷宫回溯问题分析和实现
简单找路迷宫 ```java public class MiGong {
public static void main(String[] args) {
//先创建一个二维数组,模拟迷宫//地图int[][] map = new int[8][7];//使用1表示迷宫的墙//先把上下全部置为1for (int i = 0; i < 7; i++) {map[0][i] = 1;map[7][i] = 1;}//把左右全部置为1for (int i = 0; i< 8; i++) {map[i][0] = 1;map[i][6] = 1;}//设置除外墙之外的内墙map[3][1] = 1;map[3][2] = 1;//输出地图for (int i = 0; i < 8; i++) {for (int j = 0; j < 7; j++) {System.out.print(map[i][j] + " ");}System.out.println();}//使用递归回溯,找路setWay(map,1,1);System.out.println("-----------------------");//输出路径图for (int i = 0; i < 8; i++) {for (int j = 0; j < 7; j++) {System.out.print(map[i][j] + " ");}System.out.println();}
}
//使用递归来走迷宫 //1. map表示地图 //2. i,j表示从地图的哪个位置开始出发:例如(1,1) //3. 如果小球能到达map[6][5]位置,则说明通路能找到 //4. 当map[i][j]为0表示该点没有走过,当为1表示墙,2表示通路可以走,3表示该点已经走过,但是走不通 //5. 在走迷宫时需要确定一个策略,下->右->上->左
/*
- @param map 表示地图
- @param i 从地图的i行开始
- @param j 从地图的j列开始
- @return 如果找到则返回true,否则返回false
*/
public static boolean setWay(int[][] map, int i, int j) {
if (map[6][5] == 2) { //通路已经找到ok
} else {return true;
} }if (map[i][j] == 0) { //如果当前这个点还没有走过//下->右->上->左map[i][j] = 2; //假定该点可以走通if (setWay(map, i + 1, j)) { //下return true;} else if (setWay(map, i, j + 1)) { //右return true;} else if (setWay(map, i - 1, j)) { //上return true;} else if (setWay(map, i, j - 1)) { //左return true;} else {//说明是死路map[i][j] = 3;return false;}} else { //如果map[i][j] != 0,可能是1,2,3return false;}
}
2. 找出迷宫的所有路径```javaimport java.io.BufferedReader;import java.io.FileNotFoundException;import java.io.FileReader;import java.io.IOException;public class MyMiGong {//读取迷宫的规格final static int HEIGHT = 7;final static int WIDTH = 7;public static void main(String[] args) throws IOException {//迷宫文件路径String mapFile = "Recursion/src/maps/map1.txt";int[][] map = readMap(mapFile);System.out.println("-------迷宫-------");for (int[] ints : map) {for (int anInt : ints) {System.out.print(anInt + " ");}System.out.println();}findWay(map, 1, 1);}/**** @param mapFile 文件路径字符串* @return 迷宫地图* @throws IOException*/public static int[][] readMap(String mapFile) throws IOException {int[][] map = new int[HEIGHT][WIDTH];BufferedReader br = new BufferedReader( new FileReader(mapFile));String buff = null;int i = 0;while ( (buff = br.readLine() ) != null ) {for (int j = 0; j < buff.length(); j++) {map[i][j] = Integer.parseInt(String.valueOf(buff.charAt(j)));}i++;}return map;}public static void findWay(int[][] map, int i, int j) {map[i][j] = 2;if (i == 5 && j == 5) {System.out.println("-------路径-------");for (int[] ints : map) {for (int anInt : ints) {System.out.print(anInt + " ");}System.out.println();}}//向上移动if (map[i - 1][j] == 0) {findWay(map,i-1, j);}//向左移动if (map[i][j - 1] == 0) {findWay(map, i, j-1);}//向下移动if (map[i + 1][j] == 0) {findWay(map,i+1, j);}//向右移动if (map[i][j + 1] == 0) {findWay(map,i, j+1);}map[i][j] = 0;}}
1. 八皇后问题(回溯算法)
八皇后问题介绍
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋于马克斯·贝瑟尔于1848年提出,在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能在同一行、同一列或同一斜线上,问有多少种摆法。存在92种
思路分析
- 第一个皇后先放在第一列第一行
- 第二个皇后放在第二行第一列,然后判断是否满足条件,如果不满足,继续放在第二列、第三列,依次把所有列都放完,找到一个合适的
- 继续第三个皇后,还是第一列、第二列…直到第八个皇后也能放在一个不冲突的位置,算是找到一个正确的解
- 当得到一个正确解时,在栈回退到上一个栈,就开始回溯,即将第一个皇后,放到第一列的所有解,全部得到
- 然后回头继续第一个皇后放第二列,后面继续循环执行1,2,3,4的步骤
- 补充:理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法用一个一维数组即可解决该问题,如arr[8] = {0,4,7,5,2,6,1,3},arr的下标n表示第n+1行,即也是第n+1个皇后,n所指向的数d表示第d+1列
代码实现
package queen8;import java.util.Arrays;public class Queen8 {//皇后个数final static int QUEEN = 8;//定义数组array,保存皇后的放置位置的结果,比如arr = {0,4,7,5,2,6,1,3}int[] array = new int[QUEEN];//计数static int count = 0;public static void main(String[] args) {Queen8 queen8 = new Queen8();queen8.check(0);}//编写一个方法,放置第n个皇后private void check(int n) {if (n == QUEEN) {System.out.println(++count + ": " + Arrays.toString(array));return;}//依次放入皇后,并判断是否冲突for (int i = 0; i < QUEEN; i++) {//将皇后n,放到第n行第i列array[n] = i;//判断是否冲突if (judge(n)) { //不冲突//则方n+1的皇后check(n + 1);}//冲突则放到下一列}}//查看当我们放置第n个皇后,就去检测该皇后是否和前面已经放置的皇后出图private boolean judge(int n) {for (int i = 0; i < n; i++) {//array[i] == array[n] 判断第n个皇后是否和前面的n-1个皇后在同一列//Math.abs(n - i) == Math.abs(array[n] - array[i]) 判断第n个皇后是否和第i个皇后在同一斜线上if (array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i])) {return false;}}return true;}}
排序
介绍
排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排序的过程
排序的分类
- 内部排序:指将需要处理的所有数据都加载到内部存储器中进行排序
- 外部排序法:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序
常见的排序算法分类
- 内部排序
- 插入排序:直接插入排序、希尔排序(Shell排序)
- 选择排序:简单选择排序、堆排序
- 交换排序:冒泡排序、快速排序
- 归并排序
- 基数排序
- 内部排序
算法的时间复杂度
- 度量一个程序(算法)执行时间的两种方法
- 事后统计法
- 事前估计法
- 时间频度
- 一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行的次数多,它花费时间就越多
- 一个算法中的语句执行次数称为语句频度或时间频度,记为T(n)
- 举例 ```java int toltal = 0; int end = 100; //使用for循环计算 for (int i = 1; i <= end; i++) { total+=1; }
//计算出时间赋值T(n) = n + 1
- **忽略常数项、忽略低次项、忽略系数**3. 时间复杂度- 一般情况下,算法中的基本操作语句的重复执行次数是规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度- 时间频度不同,但时间复杂度是可能相同的- 计算时间复杂度的方法- 用常数1代替运行时间中的所有加法常数- 修改后的运行次数函数中,只保留最高阶项- 去除最高阶项的系数4. 常见的时间复杂度(**由小到大**:随着问题规模n的不断增大,以下时间复杂度不断增大,算法执行的效率越低)- 常数阶O(1)- 对数阶O(logn)- 线性阶O(n)- 线性对数阶O(nlogn)- 平方阶O(n)- 立方阶O(n)- k次方阶O(n)- 指数阶O(2)5. 平均时间复杂度和最坏时间复杂度- 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况,该算法的运行时间- 最坏情况下的时间复杂度称为最坏时间复杂度,**一般讨论时间复杂度都是最坏情况下的时间复杂度**,是因为最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长- 平均时间复杂度和最坏时间复杂度是否一致,和算法有关,如下表| 排序法 | 平均时间 | 最差情形 | 稳定度 | 额外空间 | 备注 || :---: | :---: | :---: | :---: | :---: | :---: || 冒泡排序 | O(n) | O(n) | 稳定 | O(1) | n小时较好 || 交换排序 | O(n) | O(n) | 不稳定 | O(1) | n小时较好 || 选择排序 | O(n) | O(n) | 不稳定 | O(1) | n小时较好 || 插入排序 | O(n) | O(n) | 稳定 | O(1) | 大部分已排序时较好 || 基数排序 | O(log) | O(log) | 稳定 | O(n) | B是真数(0-9),R是基数(个十百) || 希尔排序 | O(nlogn) | O(n) 1<s<2 | 不稳定 | O(1) | s是所选分组 || 快速排序 | O(nlogn) | O(n) | 不稳定 | O(nlogn) | n大时较好 || 归并排序 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时较好 || 堆排序 | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大时较好 |<a name="15208fbb"></a>## 算法的空间复杂度1. 类似于时间复杂度的谈论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数1. 空间复杂度是一个算法在运行过程中临时占用存储空间大小的度量,有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序就属于这种情况1. 在做算法分析时,主要讨论的是算法时间复杂度,从用户的角度来看,更看重程序的执行速度,1. 一些缓存产品和算法本质就是用**空间换时间**<a name="bdb1d069"></a>## 1. 冒泡排序(Bubble Sort)<a name="0a480a91"></a>### 冒泡排序介绍1. 冒泡排序的基本思想是:通过对待排序序列从前向后(从下表较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒。1. **优化**:因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过减缓,从而减少不必要的比较<a name="83175ad0-1"></a>### 代码实现1. 基本实现```javapackage bubblesorting;import java.util.Arrays;public class BubbleSorting {public static void main(String[] args) {//定义需要排序的数组int arr[] = {3,9,-1,10,-2};//int arr[] = {2,1,3,4,5};//定义交换数int temp = 0;//定义优化变量boolean flag;for (int i = 0; i < arr.length - 1; i++) {flag = true;for (int j = 0; j < arr.length - 1 - i; j++) {//如果前面的数比后面的数大,则交换if (arr[j] > arr[j + 1]) {temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;flag = false;}}System.out.println("第"+ (i+1) +"排序");System.out.println(Arrays.toString(arr));if (flag) {break;}}}}
- 封装成方法以及观察冒泡排序的运行速度
package bubblesorting;import java.text.SimpleDateFormat;import java.util.Date;public class BubbleSorting {public static void main(String[] args) {//测试一下冒泡排序的速度O(n^2),给80000个数据,测试//创建存在80000个随机数的数组int[] arr = new int[80000];for (int i = 0; i < arr.length; i++) {arr[i] = (int)(Math.random() * 8000000);//产生一个0-8000000的数}Date date1 = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");String date1Str = simpleDateFormat.format(date1);System.out.println("排序前的时间是: " + date1Str);bubbleSort(arr);Date date2 = new Date();String date2Str = simpleDateFormat.format(date2);System.out.println("排序后的时间是: " + date2Str);System.out.println("总共消耗时间约为:" + ((date2.getTime() - date1.getTime()) / 1000) + "秒");}public static void bubbleSort(int[] arr) {//定义交换数int temp = 0;//定义优化变量boolean flag;for (int i = 0; i < arr.length - 1; i++) {flag = true;for (int j = 0; j < arr.length - 1 - i; j++) {//如果前面的数比后面的数大,则交换if (arr[j] > arr[j + 1]) {temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;flag = false;}}if (flag) {break;}}}}
2. 选择排序(Select Sort)
选择排序介绍
- 选择排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
- 选择排序思想是:第1次从arr[0]到arr[n-1]中选取最小值,与arr[0]交换,第2次再从arr[1]到[n-1]中选取最小值,与arr[1]交换,…,第i次从arr[i]到[n-1]中选取最小值,与arr[i]交换,…,第n-1次从arr[n-2]到arr[n-1]中选取最小值,与arr[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排序的有序序列。通俗来讲就是把每次找到的最小值与最前面(这里的最前面是指除前面找找到排好的)的那个数交换。
代码实现
- 基本实现
package selectsorting;import java.util.Arrays;public class SelectSorting {public static void main(String[] args) {int[] arr = {101,34,119,1};System.out.println("排序前:" + Arrays.toString(arr));selectSort(arr);System.out.println("排序后:" + Arrays.toString(arr));}public static void selectSort(int[] arr) {//用来交换的数int temp;//记录着数组中最小值得下标int minIndex;for (int i = 0; i < arr.length - 1; i++) {minIndex = i;for (int j = i + 1; j < arr.length; j++) {if (arr[j] < arr[minIndex]) {//得到最小值得下标minIndex = j;}}//进行数据交换(优化)if (minIndex != i) {temp = arr[i];arr[i] = arr[minIndex];arr[minIndex] = temp;}}}}
- 运行速度测试
package selectsorting;import java.text.SimpleDateFormat;import java.util.Date;public class SelectSorting {public static void main(String[] args) {int[] arr = new int[80000];for (int i = 0; i < arr.length; i++) {arr[i] = (int)(Math.random() * 8000000);//产生一个0-8000000的数}Date date1 = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");String date1Str = simpleDateFormat.format(date1);System.out.println("排序前的时间是: " + date1Str);selectSort(arr);Date date2 = new Date();String date2Str = simpleDateFormat.format(date2);System.out.println("排序后的时间是: " + date2Str);System.out.println("总共消耗时间约为:" + ((date2.getTime() - date1.getTime()) / 1000) + "秒");}public static void selectSort(int[] arr) {//用来交换的数int temp;//记录着数组中最小值得下标int minIndex;for (int i = 0; i < arr.length - 1; i++) {minIndex = i;for (int j = i + 1; j < arr.length; j++) {if (arr[j] < arr[minIndex]) {//得到最小值得下标minIndex = j;}}//进行数据交换(优化)if (minIndex != i) {temp = arr[i];arr[i] = arr[minIndex];arr[minIndex] = temp;}}}}
3. 插入排序(Insertion Sort)
插入排序介绍
- 插入式排序属于内部排序法,是对于欲排序的元素插入的方式找寻该元素的适当位置,以达到排序的目的。
- 插入排序的基本思想是:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
代码实现
- 基本实现
package insertsorting;import java.util.Arrays;public class InsertSorting {public static void main(String[] args) {int[] arr = {101,34,119,1};System.out.println("排序前:" + Arrays.toString(arr));insertSort(arr);System.out.println("排序后:" + Arrays.toString(arr));}public static void insertSort(int[] arr) {int temp;int tempIndex;for (int i = 1; i < arr.length; i++) {temp = arr[i];tempIndex = i - 1;while(tempIndex >= 0 && temp < arr[tempIndex]) {arr[tempIndex + 1] = arr[tempIndex];tempIndex--;}arr[tempIndex + 1] = temp;}}}
- 运行速度测试
package insertsorting;import java.text.SimpleDateFormat;import java.util.Date;public class InsertSorting {public static void main(String[] args) {int[] arr = new int[80000];for (int i = 0; i < arr.length; i++) {arr[i] = (int)(Math.random() * 8000000);//产生一个0-8000000的数}Date date1 = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");String date1Str = simpleDateFormat.format(date1);System.out.println("排序前的时间是: " + date1Str);insertSort(arr);Date date2 = new Date();String date2Str = simpleDateFormat.format(date2);System.out.println("排序后的时间是: " + date2Str);System.out.println("总共消耗时间约为:" + ((date2.getTime() - date1.getTime()) / 1000) + "秒");}public static void insertSort(int[] arr) {int temp;int tempIndex;for (int i = 1; i < arr.length; i++) {temp = arr[i];tempIndex = i - 1;while(tempIndex >= 0 && temp < arr[tempIndex]) {arr[tempIndex + 1] = arr[tempIndex];tempIndex--;}arr[tempIndex + 1] = temp;}}}
4. 希尔排序(Shell Sort)
希尔排序介绍
- 希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
- 希尔排序的基本思想:希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序,随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
代码实现
- 交换法
package shellsorting;import java.util.Arrays;public class ShellSorting {public static void main(String[] args) {//int[] arr = {8,9,1,7,2,3,5,4,6,0};int[] arr = new int[80000];for (int i = 0; i < arr.length; i++) {arr[i] = (int)(Math.random() * 8000000);//产生一个0-8000000的数}Date date1 = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");String date1Str = simpleDateFormat.format(date1);System.out.println("排序前的时间是: " + date1Str);shellSort1(arr);Date date2 = new Date();String date2Str = simpleDateFormat.format(date2);System.out.println("排序后的时间是: " + date2Str);System.out.println("总共消耗时间约为:" + (date2.getTime() - date1.getTime()) + "ms");}/*** 交换法* @param arr*/public static void shellSort1(int[] arr) {int temp = 0;for (int gap = arr.length / 2; gap > 0; gap /= 2) {for (int i = gap; i < arr.length; i++) {for (int j = i - gap; j >= 0; j -= gap) {if (arr[j] > arr[j + gap]) {temp = arr[j];arr[j] = arr[j + gap];arr[j + gap] = temp;}}}}}}
- 移动法
package shellsorting;import java.text.SimpleDateFormat;import java.util.Date;public class ShellSorting {public static void main(String[] args) {//int[] arr = {8,9,1,7,2,3,5,4,6,0};int[] arr = new int[80000];for (int i = 0; i < arr.length; i++) {arr[i] = (int)(Math.random() * 8000000);//产生一个0-8000000的数}Date date1 = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");String date1Str = simpleDateFormat.format(date1);System.out.println("排序前的时间是: " + date1Str);shellSort2(arr);Date date2 = new Date();String date2Str = simpleDateFormat.format(date2);System.out.println("排序后的时间是: " + date2Str);System.out.println("总共消耗时间约为:" + (date2.getTime() - date1.getTime()) + "ms");}/*** 移动法* @param arr*/public static void shellSort2(int[] arr) {//增量gap,并逐步的缩小增量for (int gap = arr.length / 2; gap > 0; gap /= 2) {//从第gap个元算,逐步对其所在的组进行直接插入排序for (int i = gap; i < arr.length; i++) {int j = i;int temp = arr[j];if (arr[j] < arr[j - gap]) {while (j - gap >= 0 && temp < arr[j - gap]) {//移动arr[j] = arr[j - gap];j -= gap;}//当退出while后,就给temp找到了最后的位置arr[j] = temp;}}}}}
- 发现移动法的效率比交换法高
5. 快速排序(Quick Sort)
快速排序介绍
- 快速排序是对冒泡排序的一种改进。
- 快速排序的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
代码实现
- 基本实现
package quicksort;import java.util.Arrays;public class QuickSort {public static void main(String[] args) {int[] arr = {-9,78,0,23,-567,70,1,5,78,454,45,45,-454,48};System.out.println("排序前:" + Arrays.toString(arr));quickSort(arr, 0, arr.length - 1);System.out.println("排序后:" + Arrays.toString(arr));}public static void quickSort(int[] arr, int left, int right) {int l = left;// 左下标int r = right;// 右下标// 中值int pivot = arr[(left + right) / 2];// 交换值int temp = 0;while (l < r) {// while的目的是让比pivot小的值放到它的左边,比pivot大的值放右边// 在pivot的左边一直找,找到大于等于pivot的值,才退出while (arr[l] < pivot) {l += 1;}// 在pivot的右边一直找,找到小于等于pivot的值,才退出while (arr[r] > pivot) {r -= 1;}// l>=r说明pivot的左右的值,已经满足pivot的右边全都大于pivot,pivot的左边全都小于pivotif (l >= r) {break;}// 交换temp = arr[l];arr[l] = arr[r];arr[r] = temp;// 如果交换完后,发现这个arr[l] == pivot值相等,则r--,前移if (arr[l] == pivot) {r -= 1;}// 如果交换完后,发现这个arr[r] == pivot值相等,则l++,前移if (arr[r] == pivot) {l += 1;}}// 如果l == r。必须l++,r--,否则会出现栈溢出if (l == r) {l += 1;r -= 1;}//向左递归if (left < r) {quickSort(arr, left, r);}//向右递归if (right > l) {quickSort(arr, l, right);}}}
- 运行速度测试
package quicksort;import java.text.SimpleDateFormat;import java.util.Date;public class QuickSort {public static void main(String[] args) {int[] arr = new int[80000];for (int i = 0; i < arr.length; i++) {arr[i] = (int)(Math.random() * 8000000);//产生一个0-8000000的数}Date date1 = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");String date1Str = simpleDateFormat.format(date1);System.out.println("排序前的时间是: " + date1Str);quickSort(arr, 0, arr.length - 1);Date date2 = new Date();String date2Str = simpleDateFormat.format(date2);System.out.println("排序后的时间是: " + date2Str);System.out.println("总共消耗时间约为:" + (date2.getTime() - date1.getTime()) + "ms");}public static void quickSort(int[] arr, int left, int right) {int l = left;// 左下标int r = right;// 右下标// 中值int pivot = arr[(left + right) / 2];// 交换值int temp = 0;while (l < r) {// while的目的是让比pivot小的值放到它的左边,比pivot大的值放右边// 在pivot的左边一直找,找到大于等于pivot的值,才退出while (arr[l] < pivot) {l += 1;}// 在pivot的右边一直找,找到小于等于pivot的值,才退出while (arr[r] > pivot) {r -= 1;}// l>=r说明pivot的左右的值,已经满足pivot的右边全都大于pivot,pivot的左边全都小于pivotif (l >= r) {break;}// 交换temp = arr[l];arr[l] = arr[r];arr[r] = temp;// 如果交换完后,发现这个arr[l] == pivot值相等,则r--,前移if (arr[l] == pivot) {r -= 1;}// 如果交换完后,发现这个arr[r] == pivot值相等,则l++,前移if (arr[r] == pivot) {l += 1;}}// 如果l == r。必须l++,r--,否则会出现栈溢出if (l == r) {l += 1;r -= 1;}//向左递归if (left < r) {quickSort(arr, left, r);}//向右递归if (right > l) {quickSort(arr, l, right);}}}
6. 归并排序(Merge Sort)
归并排序介绍
- 归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案“修补”在一起,即分为治之)
代码实现
- 基本实现
package mergesort;import java.util.Arrays;public class MergeSort {public static void main(String[] args) {int[] arr = {8,4,5,7,1,3,6,2};int[] temp = new int[arr.length];System.out.println("排序前:" + Arrays.toString(arr));mergeSort(arr,0,arr.length - 1, temp);System.out.println("排序前:" + Arrays.toString(arr));}public static void mergeSort(int[] arr, int left, int right, int[] temp) {if (left < right) {int mid = (left + right) / 2;//向左递归分解mergeSort(arr, left, mid, temp);//向右递归分解mergeSort(arr, mid+1, right, temp);//合并merge(arr, left,mid,right,temp);}}/**** @param arr 待排序的数组* @param left 左边有序序列的初始索引* @param mid 中间索引* @param right 右边索引* @param temp 中转数组*/public static void merge(int[] arr, int left, int mid, int right, int[] temp) {int i = left; // 初始化i,左边有序序列的初始索引int j = mid + 1; //初始化j,表示右边有序序列的初始索引int t = 0; // 指向temp数组的当前索引//先把左右两边的数据按规则填充到temp数组//直到左右两边有序序列有一边处理完毕while (i <= mid && j <= right) {//左边小于等于右边if (arr[i] <= arr[j]) {temp[t] = arr[i];t++;i++;} else {temp[t] = arr[j];t++;j++;}}//把有剩余数据的一边的数据依次全部填充到temp中,//左边未完while ( i <= mid) {temp[t] = arr[i];t++;i++;}//右边未完while (j <= right) {temp[t] = arr[j];t++;j++;}//将temp数组中的数据拷贝到arrt = 0;int tempLeft = left;while (tempLeft <= right) {arr[tempLeft] = temp[t];t++;tempLeft++;}}}
- 运行速度测试
package mergesort;import java.text.SimpleDateFormat;import java.util.Arrays;import java.util.Date;public class MergeSort {public static void main(String[] args) {int[] arr = new int[80000];for (int i = 0; i < arr.length; i++) {arr[i] = (int)(Math.random() * 8000000);//产生一个0-8000000的数}int[] temp = new int[arr.length];Date date1 = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");String date1Str = simpleDateFormat.format(date1);System.out.println("排序前的时间是: " + date1Str);mergeSort(arr,0,arr.length - 1, temp);Date date2 = new Date();String date2Str = simpleDateFormat.format(date2);System.out.println("排序后的时间是: " + date2Str);System.out.println("总共消耗时间约为:" + (date2.getTime() - date1.getTime()) + "ms");}public static void mergeSort(int[] arr, int left, int right, int[] temp) {if (left < right) {int mid = (left + right) / 2;//向左递归分解mergeSort(arr, left, mid, temp);//向右递归分解mergeSort(arr, mid+1, right, temp);//合并merge(arr, left,mid,right,temp);}}/**** @param arr 待排序的数组* @param left 左边有序序列的初始索引* @param mid 中间索引* @param right 右边索引* @param temp 中转数组*/public static void merge(int[] arr, int left, int mid, int right, int[] temp) {int i = left; // 初始化i,左边有序序列的初始索引int j = mid + 1; //初始化j,表示右边有序序列的初始索引int t = 0; // 指向temp数组的当前索引//先把左右两边的数据按规则填充到temp数组//直到左右两边有序序列有一边处理完毕while (i <= mid && j <= right) {//左边小于等于右边if (arr[i] <= arr[j]) {temp[t] = arr[i];t++;i++;} else {temp[t] = arr[j];t++;j++;}}//把有剩余数据的一边的数据依次全部填充到temp中,//左边未完while ( i <= mid) {temp[t] = arr[i];t++;i++;}//右边未完while (j <= right) {temp[t] = arr[j];t++;j++;}//将temp数组中的数据拷贝到arrt = 0;int tempLeft = left;while (tempLeft <= right) {arr[tempLeft] = temp[t];t++;tempLeft++;}}}
7.基数排序(Radix Sort)
基数排序介绍
- 基数排序属于“分配式排序”(Distribution Sort),又称“桶子法”(Bucket Sort),它是通过键值的各个位的值,将要排序的元素分配至”桶“中,达到排序的作用
- 基数排序法是属于稳定性的排序,基数排序法是效率高的稳定性排序法
- 基数排序是“桶排序”的扩展,速度很快
- 技术排序是1887年赫尔曼·何乐礼发明的,将证书按位数切割成不同的数字,然后按每个位数分别比较
- 基数排序的基本思想是:将所有待比较数值同意为同样长度,数位较短的数前面补零,然后从最低位开始,依次进行一次排序,这样从最低位排序一直到最高位排序完成以后,数列就变成了一个有序序列
- 基数排序是经典的空间换时间的方式,占内存很大,当对海量数据排序时,容易造成OutOfMemoryError
- 有负数的数组,我们不用基数排序来进行排序,如果需要支持负数,参考https://code.i-harness.com/zh-CN/q/e98fa9
代码实现
- 基本实现
package radixsort;import java.util.Arrays;public class RadixSort {public static void main(String[] args) {int[] arr = {53,3,542,748,14,214};System.out.println("排序前:" + Arrays.toString(arr));radixSort(arr);System.out.println("排序前:" + Arrays.toString(arr));}public static void radixSort(int[] arr) {//定义10个桶int[][] bucket = new int[10][arr.length];//为了记录每个桶中实际存放了多少个数据,定义一个一维数组,记录各个桶每次放入的数据个数int[] bucketElementCounts = new int[10];//得到数组中最大数据的位数int max = arr[0];// 假设是第一个for (int i = 1; i < arr.length; i++) {if (arr[i] > max) {max = arr[i];}}int maxLength = (max + "").length();for (int i = 0; i < maxLength; i++) {for (int j = 0; j < arr.length; j++) {//取出每个元素的个位数字int digitOfElement = arr[j] / ((int)Math.pow(10, i)) % 10;//放在对应的桶中bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];bucketElementCounts[digitOfElement]++;}//将桶中的数据放会arr中int index = 0;for (int k = 0; k < bucket.length; k++) {//如果桶中存在数据,才放回原数组if (bucketElementCounts[k] != 0) {//遍历第k个桶for (int l = 0; l < bucketElementCounts[k]; l++) {//取出数据,将数据放回arrarr[index] = bucket[k][l];index++;}}//需要将bucketElementCounts[k]重新置为0!!!bucketElementCounts[k] = 0;}}}}
- 运行速度测试
package radixsort;import java.text.SimpleDateFormat;import java.util.Date;public class RadixSort {public static void main(String[] args) {int[] arr = new int[80000];for (int i = 0; i < arr.length; i++) {arr[i] = (int)(Math.random() * 8000000);//产生一个0-8000000的数}Date date1 = new Date();SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");String date1Str = simpleDateFormat.format(date1);System.out.println("排序前的时间是: " + date1Str);radixSort(arr);Date date2 = new Date();String date2Str = simpleDateFormat.format(date2);System.out.println("排序后的时间是: " + date2Str);System.out.println("总共消耗时间约为:" + (date2.getTime() - date1.getTime()) + "ms");}public static void radixSort(int[] arr) {//定义10个桶int[][] bucket = new int[10][arr.length];//为了记录每个桶中实际存放了多少个数据,定义一个一维数组,记录各个桶每次放入的数据个数int[] bucketElementCounts = new int[10];//得到数组中最大数据的位数int max = arr[0];// 假设是第一个for (int i = 1; i < arr.length; i++) {if (arr[i] > max) {max = arr[i];}}int maxLength = (max + "").length();for (int i = 0; i < maxLength; i++) {for (int j = 0; j < arr.length; j++) {//取出每个元素的个位数字int digitOfElement = arr[j] / ((int)Math.pow(10, i)) % 10;//放在对应的桶中bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];bucketElementCounts[digitOfElement]++;}//将桶中的数据放会arr中int index = 0;for (int k = 0; k < bucket.length; k++) {//如果桶中存在数据,才放回原数组if (bucketElementCounts[k] != 0) {//遍历第k个桶for (int l = 0; l < bucketElementCounts[k]; l++) {//取出数据,将数据放回arrarr[index] = bucket[k][l];index++;}}//需要将bucketElementCounts[k]重新置为0!!!bucketElementCounts[k] = 0;}}}}
查找
介绍
在java中,我们常用的查找有4种
- 顺序(线性)查找
- 二分查找/折半查找
- 插值查找
- 斐波那契查找
1. 线性查找(Linear Search)
线性查找介绍
- 查找的序列可也是有序的,也可以是无序的
- 线性查找思路:线性查找是逐一比对,发现有相同值,就返回下标
代码实现
package linearsearch;import java.util.Arrays;public class LinearSearch {public static void main(String[] args) {int[] arr = {1,9,11,-1,34,89}; //可有序,可无序int index, value;System.out.println("arr = " + Arrays.toString(arr));value = 11;index = linearSearch(arr, value);if (index == -1) {System.out.println("没有找到" + value);} else {System.out.println("下标为" + index);}}/*** 这个方法是找到一个满足条件就返回* @param arr 查询数组* @param value 查询值* @return 查询值在查询数组中的索引*/public static int linearSearch(int[] arr, int value) {//线性查找是逐一比对,发现有相同值,就返回下标for (int i = 0; i < arr.length; i++) {if (arr[i] == value) {return i;}}return -1;}}
2. 二分查找(Binary Search)
二分查找介绍
- 查找的序列必须是有序的,要查找需要将序列进行排序
- 发现有相同值,就返回下标
- 存在两种方法,递归和非递归
- 二分查找的思路分析(假设arr数组是升序):
- 首先确定该数组的中间的下标,mid = (left + right) / 2
- 然后让需要查找的数findValue和arr[mid]作比较
- 若indValue>arr[mid],则indValue在arr[mid]的右边,反之则在左边,若indValue==arr[mid],则返回mid。进行递归操作
- 考虑到找不到时,当left>right就会退出递归
代码实现
- 基本实现
package binarysearch;import java.util.Arrays;public class BinarySearch {public static void main(String[] args) {// 使用二分查找的前提,该数组是有序的int[] arr = {1,8,10,89,1000,1234};int index, value;System.out.println("arr = " + Arrays.toString(arr));// value = 9;value = 10;index = binarySearch(arr, 0, arr.length - 1, value);if (index == -1) {System.out.println("没有找到" + value);} else {System.out.println("下标为" + index);}}/**** @param arr 数组* @param left 左边的索引* @param right 右边的索引* @param value 查找的值* @return 如果找到就返回该值在数组里的索引,没找到就返回-1*/public static int binarySearch(int[] arr, int left, int right, int value) {// 没找到的退出递归条件if (left > right) {return -1;}int mid = (left + right) / 2;int midValue = arr[mid];if (value < midValue) {// 向左递归return binarySearch(arr, left, mid - 1, value);} else if (value > midValue) {// 向右递归return binarySearch(arr, mid + 1, right, value);} else {// 找到了return mid;}}}
- 当一个有序数组中存在多个相同值时,将所有数值都查找到
package binarysearch;import java.util.ArrayList;import java.util.Arrays;public class BinarySearch {public static void main(String[] args) {// 使用二分查找的前提,该数组是有序的int[] arr = {1,8,10,89,1000,1000,1000,1000,1234};int index, value;System.out.println("arr = " + Arrays.toString(arr));// value = 9;value = 1000;ArrayList<Integer> indexes = binarySearch2(arr, 0, arr.length - 1, value);if (indexes.size() == 0) {System.out.println("没有找到" + value);} else {System.out.println(value + "下标为" + indexes);}}//当一个有序数组中存在多个相同值时,将所有数值都查找到,返回所有下标/*** 在原先二分法进行优化,在找到匹配数值时,不要一找到就返回* 向mid索引值的左边扫描,将所有满足等于查找值的下标加入到ArrayList,右边同理* @param arr 数组* @param left 左边的索引* @param right 右边的索引* @param value 查找的值* @return*/public static ArrayList<Integer> binarySearch2(int[] arr, int left, int right, int value) {// 没找到的退出递归条件if (left > right) {return new ArrayList<Integer>();}int mid = (left + right) / 2;int midValue = arr[mid];if (value < midValue) {// 向左递归return binarySearch2(arr, left, mid - 1, value);} else if (value > midValue) {// 向右递归return binarySearch2(arr, mid + 1, right, value);} else {// 找到了ArrayList<Integer> list = new ArrayList<>();//向左边扫描int temp = mid - 1;while (true) {if (temp < 0 || arr[temp] != value) {break;}list.add(temp);temp--;}list.add(mid);//向右边扫描temp = mid + 1;while (true) {if (temp > arr.length - 1 || arr[temp] != value) {break;}list.add(temp);temp++;}return list;}}}
3. 插值查找(Interpolation Search)
插值查找介绍
- 插值查找原理如下:
- 插值查找算法类似于二分查找,不同的是插值查找每次从自适应mid开始查找
- 将折半查找中的求mid索引的公式,low表示左边索引left,high表示右边索引right
%0A#card=math&code=mid%3D%5Cfrac%7Blow%2Bhigh%7D%7B2%7D%3Dlow%2B%5Cfrac%7B1%7D%7B2%7D%28high-low%29%0A)
改成下列式子(key代表要查找的值),自适应%0A#card=math&code=mid%3Dlow%2B%5Cfrac%7Bkey-a%5Blow%5D%7D%7Ba%5Bhigh%5D-a%5Blow%5D%7D%28high-low%29%0A)
- 举例说明插值查找算法1-100的数组,假如我们要查找的值为1
- 使用二分查找,我们要进行多次递归,才能找到1
- 使用插值查找,第一次mid=0+((1-1)/(100-1))*(99-0)=0,而a[mid]=a[0]=1,一次就直接找到了1
- 插值查找注意事项
- 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找,速度较快
- 关键字分布不均匀的情况下,该方法不一定比折半查找好
代码实现
- 基本实现
package interpolationsearch;import java.util.Arrays;public class InterpolationSearch {public static void main(String[] args) {//初始化数组int[] arr = new int[100];for (int i = 0; i < 100; i++) {arr[i] = i + 1;}int index, value;System.out.println("arr = " + Arrays.toString(arr));value = 2;index = interpolationSearch(arr, 0, arr.length - 1, value);if (index == -1) {System.out.println("没有找到" + value);} else {System.out.println("下标为" + index);}}/*** 插值查找算法,也要求数组是有序的* @param arr 数组* @param left 左边索引* @param right 右边索引* @param value 查找值* @return 返回查找值在arr数组中的下标,若没找到,则返回-1*/public static int interpolationSearch(int[] arr, int left, int right, int value) {//注意:value < arr[0] || value > arr[arr.length - 1 必须要,否则可能会越界if (left > right || value < arr[0] || value > arr[arr.length - 1]) {return -1;}// 求出midint mid = left + (right - left) * ((value - arr[left]) / (arr[right] - arr[left]));int midValue = arr[mid];if (value < midValue) {// 左递归return interpolationSearch(arr, left, mid - 1, value);} else if (value > midValue) {// 右递归return interpolationSearch(arr, mid + 1, right, value);} else {return mid;}}}
4. 斐波那契查找(Fibonacci Search)
斐波那契查找介绍
- 斐波那契又称为黄金分割法
- 黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意想不到的效果。
- 斐波那契数列{1,1,2,3,5,8,13,21,34,55}发现斐波那契数列的两个相邻数的比例,无限接近黄金分割值0.618
- 斐波那契(黄金分割法)原理:
- 斐波那契原理与二分查找、插值查找相似,仅仅改变了中间结点(mid)的位置,mid不再是中间或插值得到的,而是位于黄金分割点附近,即mid=low+F(k-1)-1,F代表斐波那契数列
- 对F(k-1)-1的理解:
- 由斐波那契数列F[k]=F[k-1]+F[k-2]的性质,可以得到(F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1。该式说明:只要顺序表的长度为F[k]-1,则可以将该表分成长度为F[k-1]-1和F[k-2]-1的两段,即如下图所示。从而中间位置为mid=low+F(k-1)-1

- 类似的,每一子段也可以用相同的方式分割- 但顺序表长度n不一定刚好等于F[k]-1,所以需要将原来的顺序表长度n增加至F[k]-1。这里的k值只要能使得F[k]-1恰好大于或等于n即可,由以下代码得到,顺序表长度增加后,新增的位置(从n+1到F[k]-1位置),都赋为n位置的值即可。
while(n > fib(k) - 1)k++;
代码实现
- 基本实现
package fibonaccisearch;import java.util.Arrays;public class FibonacciSearch {private final static int MAXSIZE = 20;public static void main(String[] args) {int[] arr = {1,8,10,89,100,1234};int index, value;value = 8;index = fibonacciSearch(arr, value);if (index == -1) {System.out.println("没有找到" + value);} else {System.out.println("下标为" + index);}}//因为会使用到斐波那契数列,因此我们先获取到斐波那契数列//用非递归的方式得到斐波那契数列public static int[] fib() {int[] f = new int[MAXSIZE];f[0] = 1;f[1] = 1;for (int i = 2; i < MAXSIZE; i++) {f[i] = f[i - 1] + f[i - 2];}return f;}//编写斐波那契查找算法//使用非递归的方式编写算法/**** @param arr 数组* @param value 查找值* @return 找到就返回查找值得下标,若没找到就返回-1*/public static int fibonacciSearch(int[] arr, int value) {int low = 0;int high = arr.length - 1;int k = 0; //表示斐波那契分割数值的下标int mid = 0; //存放mid的值int[] f = fib();//获取斐波那契数列//获取斐波那契分割数值的下标while (high > f[k] - 1) {k++;}//因为f[k]可能大于数组arr的长度,因此需要使用Arrays,构造一个新的数组,并指向arr//不足的部分会使用0填充int[] temp = Arrays.copyOf(arr, f[k]);//填充值for (int i = high + 1; i < temp.length; i++) {temp[i] = arr[high];}//使用while循环处理,来找到valuewhile (low <= high) {mid = low + f[k-1]-1;if (value < temp[mid]) {// 向数组的前部分查找(左边)high = mid - 1;//为什么是k--//1. 全部元素 = 前面的元素 + 后边的元素//2. f[k] = f[k-1] + f[k-2]//3. 因为前面有f[k-1]个元素,所以可以继续拆分f[k-1] = f[k-2] + f[k-3]//4. 即在f[k-1]的前面继续查找,所以是k--//5. 即下次循环mid = low + f[k-1-1]-1;k--;} else if (value > temp[mid]) {// 向数组的后部分查找(右边)low = mid + 1;//为什么是k-2//1. 全部元素 = 前面的元素 + 后边的元素//2. f[k] = f[k-1] + f[k-2]//3. 因为后面有f[k-2]个元素,所以可以继续拆分f[k-2] = f[k-3] + f[k-4]//4. 即在f[k-2]的前面继续查找,所以是k-=2//5. 即下次循环mid = low + f[k-1-2]-1;k -=2;} else { //找到if (mid <= high) {return mid;} else {return high;}}}return -1;}}
