文章目录
02 查找与排序(上)
1 递归、查找与排序补充
1 递归
递归设计经验
找重复(子问题)
找重复中的变化量→参数
找参数变化趋势→设计出口
练习策略
循环改递归-经典递归
大量练习,总结规律,掌握套路
找到感觉,挑战高难度
package org.lanqiao.algo.elementary._02_01_recursion;
import org.lanqiao.algo.util.Util;
public class _00_什么是递归 {
//注意死循环
static void f(int i) {
if (i == 0)
return;
//调用自身
f(i - 1);
}
/**
* f1(n):求n的阶乘-->f1(n-1)求n-1的阶乘
* 找重复:n*(n-1)的阶乘,求n-1的阶乘是原问题的重复(规模更小)——子问题
* 找变化:变化的量应该作为参数
* 找边界:出口*/
static int f1(int n) {
if (n == 1)
return 1;
return n * f1(n - 1);
}
/**
* 打印i到j
* 找重复:
* 找变化:变化的量应该作为参数
* 找边界:出口*/
static void f2(int i, int j) {
if (i > j)
return;
System.out.println(i);
f2(i + 1, j);
}
/**
* 对数组arr的所有元素求和
* 找重复:
* 找变化:变化的量应该作为参数
* 找边界:出口
* @param arr
*/
static int f3(int[] arr, int begin) {
if (begin == arr.length - 1) {
return arr[begin];
}
return arr[begin] + f3(arr, begin + 1);
}
/**
* 翻转字符串
* @param src
* @param end
* @return
*/
static String reverse(String src, int end) {
if (end == 0) {
return "" + src.charAt(0);
}
return src.charAt(end) + reverse(src, end - 1);
}
/**
*斐波拉契序列化为多个小规模
*递归的二叉树
* O(2^n)
* @param n
* @return
*/
static int fib(int n) {
if (n == 1 || n == 2)
return 1;
return fib(n - 1) + fib(n - 2);
}
/**
* 求解最大公约数
* @param m
* @param n
* @return
*/
static int gcd(int m, int n) {
if (n == 0)
return m;
return gcd(n, m % n);
}
/**
* 递归形式进行插入排序
* @param arr
* @param k
* @return
*/
static void insertSort(int[] arr, int k) {
if (k == 0) {
return;
}
//对前k-1个元素排序
insertSort(arr, k - 1);
//把位置k的元素插入到前面的部分
int x = arr[k];
int index = k - 1;
while (index > -1 && x < arr[index]) {
arr[index + 1] = arr[index];
index--;
}
arr[index + 1] = x;
}
public static void main(String[] args) {
// f2(8,10);
// int res = f3(new int[]{1, 2, 3, 4, 5}, 0);
// System.out.println(res);
// System.out.println(reverse("abcd", 3));
long now = System.currentTimeMillis();
System.out.println(fib(50));
Util.duration(now);
// System.out.println("gcd:" + gcd(16, 12));
// int[] arr = {2, 3, 1, 5, 4};
// insertSort(arr, 4);
// Util.print(arr);
}
}
汉诺塔问题
汉诺塔:汉诺塔(Tower of Hanoi)源于印度传说中,大梵天创造世界时造了三根金钢石柱子,其中一根柱子自底向上叠着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
思路
1-N从A移动到B,C作为辅助
等价于:
1、1~N-1从A移动到C,B为辅助
2、把N从A移动到B
3、1-N-1从C移动到B,A为辅助
package org.lanqiao.algo.elementary._02_01_recursion;
/**
* 汉诺塔递归解法
*/
public class _01_TowerOfHanoi {
public static void main(String[] args) {
printHanoiTower(20, "A", "B", "C");
}
/**
* 将N个盘子从source移动到target的路径的打印
*
* @param N 初始的N个从小到达的盘子,N是最大编号
* @param from 原始柱子
* @param to 辅助的柱子
* @param help 目标柱子
*/
static void printHanoiTower(int N, String from, String to, String help) {
if (N == 1) {
System.out.println("move " + N + " from " + from + " to " + to);
return;
}
printHanoiTower(N - 1, from, help, to); // 先把前N-1个盘子挪到辅助空间上去
System.out.println("move " + N + " from " + from + " to " + to); // N可以顺利到达target
printHanoiTower(N - 1, help, to, from); // 让N-1从辅助空间回到源空间上去
}
}
2 查找
全范围内二分查找
等价于三个子问题:
左边找(递归)
中间比
右边找(递归)
注意:左查找和右查找只选其一
package org.lanqiao.algo.elementary._02searchAndSort;
import org.lanqiao.algo.util.Util;
import java.util.Arrays;
/*
二分查找
*/
public class _0BinarySearch {
private static int binarySearch0(int[] arr, int low, int high,
int key) {
while (low <= high) {
int mid = low + ((high - low) >> 1);//(low + high) >>> 1;//防止溢出,移位也更高效。同时,每次循环都需要更新。
int midVal = arr[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
/*递归解法*/
private static int binarySearch1(int[] arr, int low, int high, int key) {
if (low > high)
return -1;
int mid = low + ((high - low) >> 1);//(low + high) >>> 1;//防止溢出,移位也更高效。
int midVal = arr[mid];
if (midVal < key)
return binarySearch1(arr, mid + 1, high, key);
else if (midVal > key)
return binarySearch1(arr, low, high - 1, key);
else
return mid; // key found
}
/**
* 顺序查找
* @param arr
* @param key
* @return
*/
private static int search(int[] arr, int key) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == key)
return i;
}
return -1;
}
public static void main(String[] args) {
int[] x = new int[10000 * 10000];
for (int i = 0; i < x.length; i++) {
x[i] = i + 1;
}
int target = 100000 * 10000;
long now = System.currentTimeMillis();
int index = binarySearch0(x, 0, x.length - 1, target);
// Util.duration(now);
System.out.println(System.currentTimeMillis() - now + "ms");
System.out.println(target + "所在位置为:" + index);
now = System.currentTimeMillis();
//调用顺序查找
index = search(x, target);
Util.duration(now);
}
}
3 排序
1 冒泡排序
package org.lanqiao.algo.elementary._02searchAndSort;
import org.lanqiao.algo.util.Util;
import java.util.Arrays;
/**
* 冒泡排序算法<br />
* 思路:第一趟,通过两两交换的手段,将最大元素顶到最末端<br />
* 第二趟,…………………………………………,将次大元素顶到倒数第二个位置<br />
* ……<br />
* 时间复杂度:O(n²)<br />
* 空间复杂度:O(1)<br />
* 原址排序<br />
* 稳定性:有相同元素,排序前和排序后相对位置不会变化,稳定<br />
*/
public class _1BubbleSort {
static void sort1(int[] arr) {
for (int j = 0; j < arr.length; j++) {
//注意上边界,泡都从第一个开始冒,但是每一趟完成之后,天花板在降低
for (int i = 0; i < arr.length - j - 1; i++) {
if (arr[i] > arr[i + 1]) {
Util.swap(arr, i, i + 1);
}
}
}
}
// 细微的改进
static void sort2(int[] arr) {
for (int j = 0; j < arr.length; j++) {
//标记这一趟是不是有交换,如果没有交换,顺序就已经排好了
boolean ordered = true;
//注意上边界,泡都从第一个开始冒,但是每一趟完成之后,天花板在降低
for (int i = 0; i < arr.length - j - 1; i++) {
if (arr[i] > arr[i + 1]) {
Util.swap(arr, i, i + 1);
ordered = false;
}
}
if (ordered) {
break;
}
}
}
public static void main(String[] args) {
/*耗时,N * N ≈ 100000*100000 10的10次方 */
int[] arr = Util.getRandomArr(100000, 1, 1000);
// System.out.println("begin..." + Arrays.toString(arr));
long now = System.currentTimeMillis();
sort2(arr);//冒泡排序
Util.duration(now);
// System.out.println("final..." + Arrays.toString(arr));
/*试试jdk的排序耗时,N*LgN ≈ 100000*17*/
arr = Util.getRandomArr(100000, 1, 1000);
now = System.currentTimeMillis();
Arrays.sort(arr);
Util.duration(now);
}
}
2 选择排序
package org.lanqiao.algo.elementary._02searchAndSort;
import org.assertj.core.api.Assertions;
import org.assertj.core.util.Preconditions;
import org.lanqiao.algo.util.Util;
/**
* 思路:第一趟,选择所有元素中最小的,和第一位交换 <br />
* 第二趟,选择第二位及以后所有元素中最小的,和第二位交换<br />
* ……<br />
* 时间复杂度:O(n²)<br />
* 空间复杂度:O(1)<br />
* 原址排序<br />
* 稳定性:考虑32211,第一趟标记最末1为最小→1(4)221(3)3;第二趟标记原下标3的1为最小→1(4)1(3)223<br />
* 两个1的相对位置发生了变化,因此不稳定<br />
*/
public class _2SelectionSort {
private static void sort(int[] arr, int low, int high) {
Preconditions.checkArgument(low <= high, "参数不正确,请确认low<=high");
Preconditions.checkNotNull(arr);
for (int i = low; i < high; i++) {
int min = i;
for (int j = min + 1; j <= high; j++) {
if (arr[min] > arr[j])
min = j; // 覆盖
}
Util.swap(arr, i, min); // 交换
}
}
public static void main(String[] args) {
int[] arr = {4, 5, 6, 3, 2, 1};
sort(arr, 0, arr.length - 1);
Assertions.assertThat(Util.arrayToString(arr)).isEqualTo("123456");
arr = new int[]{1};
sort(arr, 0, arr.length - 1);
Assertions.assertThat(Util.arrayToString(arr)).isEqualTo("1");
}
}
3 插入排序
package org.lanqiao.algo.elementary._02searchAndSort;
import org.lanqiao.algo.util.Util;
import java.util.Arrays;
/**
插入排序算法:<br />
思路:假设某元素之前的子序列有序,该元素如果大于子序列末端元素则可继续下一个元素;如果该元素小于子序列末端,则往前插入到指定位置<br />
第一趟:第一个元素已经有序<br />
第二趟:第二个元素往前插<br />
第三趟:第三个元素往前两个元素插<br />
……<br />
* 时间复杂度:O(n²)=1+2+3+...+n-1<br />
* 空间复杂度:O(1)<br />
* 原址排序<br />
* 稳定性:由于是从后往前,后续元素如果存在前面相等的,无法越过,相对位置不会发生变化<br />
*/
public class _3InsertionSort {
static void sort(int[] arr) {
sort( arr, 0, arr.length - 1 );
}
public static void sort(int[] arr, int low, int high) {
for (int j = low + 1; j <= high; j++) {
int key = arr[j];
int pre = j - 1;
/*套路:大的元素往后移直到前方没有大的,这时插入待排元素*/
while (pre >= low && arr[pre] > key) {
arr[pre + 1] = arr[pre];
pre--;
}
arr[pre + 1] = key;
}
}
/*反向排序*/
static void sortInverse(int[] arr, int low, int high) {
for (int j = low + 1; j <= high; j++) {
int key = arr[j];
int pre = j - 1;
/*只需颠倒一下符号即可*/
while (pre >= low && arr[pre] < key) {
arr[pre + 1] = arr[pre];
pre--;
}
arr[pre + 1] = key;
}
}
public static void main(String[] args) {
int[] arr = Util.getRandomArr( 20, 1, 100 );
System.out.println( "begin..." + Arrays.toString( arr ) );
sort( arr );
System.out.println( "final..." + Arrays.toString( arr ) );
}
}
4 希尔排序
希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
* 希尔排序是插入排序的一种。<br/>
* 也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法<br/>
*<br/>
* 思路:如序列 9 8 7 6 5 4 3 2 1<br/>
* 确定一个增量序列,如 4(length/2) 2 1 ,从大到小使用增量<br/>
* 使用第一个增量,将序列划分为若干个子序列,下标组合为0-4-8,1-5,2-6,3-7<br/>
* 依次对子序列使用直接插入排序法;<br/>
* 使用第二个增量,将序列划分为若干个子序列(0-2-4-6-8),(1-3-5-7)<br/>
* 依次对子序列使用直接插入排序法:<br/>
* 使用第三个增量1,这时子序列就是元序列(0-1-2-3-4-5-6-7-8),使用直接插入法<br/>
* 完成排序。<br/>
* 时间复杂度:不太确定在O(nlogn)~O(n²)之间<br />
* 空间复杂度:O(1)<br />
* 原址排序<br />
* 稳定性:由于相同的元素可能会被划分至不同子序列单独排序,因此稳定性是无法保证的——不稳定<br />
*
* */
public class _4ShellSort {
public static void main(String[] args) {
int[] arr = {9, 9, 6, 7, 5, 4, 2, 2, 1};
shellSort(arr);
Util.print(arr);
}
public static void shellSort(int[] arr) {
//不断地缩小增量
for (int interval = arr.length / 2; interval > 0; interval = interval / 2) {
//增量为interval的插入排序
for (int i = interval; i < arr.length; i++) {
int target = arr[i];
int j = i - interval;
while (j > -1 && target < arr[j]) {
arr[j + interval] = arr[j];
j -= interval;
}
arr[j + interval] = target;
}
}
}
}
2 如何评估算法性能
1思考
如何评估一个算法的性能?什么是大0表示法?
数组有序,顺序查找和二分查找哪个更快,快多少?
这世界上存在常数阶的算法吗?O(1)
for(int i=i;i<=n;i++){
k=k+5;
}
//O(n)线性复杂度
for(int i=1;i<=n;i++){
for(int j=1;i<=n;j++){
k=k+i+j;
}
}
//O(n^2)平方复杂度
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
k=k+i+j;
}
}
//O(n^2)平方复杂度
int count=1;
while(count<n){
count=count*2;
}
//O(lgN)对数复杂度,步数减少
2常见函数的复杂度计算
算法复杂度\拥有的时间 | 1s可处理的规模 |
---|---|
lgn | 2^(100000000) |
n | 1000000000 |
n^2 | 10000 |
n^3 | 500 |
2*n | 27 |
-
n!的弱上界是n^n,因此增长速度非常快,这意味着单位时间内可求解的问题很小,换言之,超慢
-
2^n这样的指数函数增长非常快,这种算法可以认为超慢
-
O(n2)和O(n3)增长很快,算法很慢,至少优化到nlgn, O(n^2)的有冒泡排序,直接插入排序,选择排序
-
nlgn可以认为是及格的算法吧,一般分治法可以缩小层数为lgn,而每层的复杂度一般为O(n),例如归并排序算法、快速排序算法
-
O(n)叫做线性算法,这种算法比较优秀,或者问题本身比较简单,比如求连续求和最大子数组的线性解
-
O(sqrt(n))当然比O(n)更快,不是没有, 但这种很少
-
lgn就是很优秀的算法了,比如二分查找法,但是这种算法往往对输入数据的格式是有要求的,二分查找要求输入数据有序
-
还有一种是常量,无论规模怎么扩大,都花固定时间,这是为数极少的效率最高的算法了,多数是数据很规则
3 排序算法的稳定性
➢稳定:如果a原本在b前面,而a=b ,排序之后a仍然在b的前面。
➢不稳定:如果a原本在b的前面,而a=b ,排序之后a可能会出现在b的后面。
排序方法 | 时间复杂度(平均) | 时间复杂度(最坏) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(n^2) | O(n) | O(1) | 不稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlog2(n)) | O(nlog2(n)) | O(nlog2(n)) | O(1) | 不稳定 |
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
快速排序 | O(nlog2(n)) | O(n^2) | O(nlog2(n)) | O(nlog2(n)) | 不稳定 |
归并排序 | O(nlog2(n)) | O(nlog2(n)) | O(nlog2(n)) | O(n) | 稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
桶排序 | O(n+k) | O(n^2) | O(n) | O(n+k) | 稳定 |
基数排序 | O(n+k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
3相关题解
题1:小白上楼梯(递归设计)
➢小白正在上楼梯,楼梯有n阶台阶,小白一次可以上1阶, 2阶或者3阶,实现一个方法,计算小白有多少种走完楼梯的方式。
package org.lanqiao.algo.elementary._02searchAndSort;
import java.util.Scanner;
public class Case01_小白上楼梯 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (true) {
int n = sc.nextInt();
int res = f(n);
System.out.println(res);
}
}
private static int f(int n) {
if (n == 0) return 1;
if (n == 1) return 1;
if (n == 2) return 2;
return f(n - 1) + f(n - 2) + f(n - 3);
}
}
题2:旋转数组的最小数字(改造二分法)
➢把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一 个旋转,该数组的最小值为1.
package org.lanqiao.algo.elementary._02searchAndSort;
import org.assertj.core.api.Assertions;
/**
* 旋转数组的最小数字:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
* 输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,
* 该数组的最小值为1.
*
* 考点:活用二分查找
*/
public class Case02_旋转数组的最小数字 {
static int min(int[] arr) {
int begin = 0;
int end = arr.length - 1;
//考虑没有旋转这种特殊的旋转
if (arr[begin] < arr[end]) return arr[begin];
//begin和end指向相邻元素,退出
while (begin + 1 < end) {
int mid = begin + ((end - begin) >> 1);
// 要么左侧有序,要么右侧有序
if (arr[mid] >= arr[begin]) {//左侧有序
begin = mid;
} else {
end = mid;
}
}
return arr[end];
}
public static void main(String[] args) {
int[] arr = {5, 1, 2, 3, 4};
int res = min(arr);
Assertions.assertThat(res).isEqualTo(1);
arr = new int[]{2, 3, 4, 5, 6};
res = min(arr);
Assertions.assertThat(res).isEqualTo(2);
arr = new int[]{1, 0, 1, 1, 1};
res = min(arr);
Assertions.assertThat(res).isEqualTo(0);
}
/*测试下{10111}:{01111}的一个旋转(顺序扫描法)*/
}
题3 :在有空字符串的有序字符串数组中查找
➢有个排序后的字符串数组,其中散布着一些空字符串 ,编写一个方法,找出给定字符串(肯定不是空字符串)的索引。
package org.lanqiao.algo.elementary._02searchAndSort;
/**
* 有个排序后的字符串数组,其中散布着一些空字符串,编写一个方法,
* 找出给定字符串(肯定不是空字符串)的索引。
* */
public class Case03_特殊有序数组中查找 {
public static void main(String[] args) {
String[] arr = {"a", "", "ac", "", "ad", "b", "", "ba"};
int res = indexOf(arr, "abc");
System.out.println(res);
}
private static int indexOf(String[] arr, String p) {
int begin = 0;
int end = arr.length - 1;
while (begin <= end) {
int indexOfMid = begin + ((end - begin) >> 1);
while (arr[indexOfMid].equals("")) {
indexOfMid++;
//千万要注意
if (indexOfMid > end)
return -1;
}
if (arr[indexOfMid].compareTo(p) > 0) {
end = indexOfMid - 1;
} else if (arr[indexOfMid].compareTo(p) < 0) {
begin = indexOfMid + 1;
} else {
return indexOfMid;
}
}
return -1;
}
}
题4 :最长连续递增子序列(部分有序)
➢(1,9,2,5,7,3,4,6,8,0)中最长的递增子序列为(3,4,6,8)。
题5:快速设计一个高效的求a的n次幂的算法
package org.lanqiao.algo.elementary._02searchAndSort;
public class Case05_a的n次幂 {
public static void main(String[] args) {
int n = 15;
int a = 2;
int res = pow0(a, n);
System.out.println(res);
res = pow(a, -1);
System.out.println(res);
}
//O(n)
private static int pow0(int a, int n) {
int res = 1;
for (int i = 0; i < n; i++) {
res *= a;
}
return res;
}
private static int pow(int a, int n) {
if (n == 0) return 1;
int res = a;
int ex = 1;
//能翻
while ((ex << 1) <= n) {
//翻
res = res * res;
//指数
ex <<= 1;
}
//不能翻
//差n-ex次方没有去乘到结果里面
return res * pow(a, n - ex);
}
}