递归、查找、排序 上
1. 递归
-
找重复
- 找到一种划分方法
- 找到递推公式或者等价转换
-
找变化
在变的量一般是作参数的
-
找边界
简单基础题(练习三个步骤)
-
求n的阶乘
public class demo01 { public static void main(String[] args) { int a = f1(4); System.out.println(a); } // 计算阶乘值 public static int f1(int n) { if(n == 1) return 1; return n*f1(n-1); } }
-
打印 i 到 j
public class demo02 { public static void main(String[] args) { f2(4,8); } // 打印出i到j public static void f2(int i, int j) { if(i > j) return; System.out.print(i+" "); f2(i+1,j); } }
-
对arr的所有元素求和
public class demo03 { public static void main(String[] args) { int[] arr = new int[]{1, 2, 3, 4, 5}; int res = f3(arr, 0); System.out.println(res); } // 对arr的所有元素求和 public static int f3(int[] arr, int begin) { if(begin == arr.length-1) return arr[begin]; return arr[begin] + f3(arr, begin+1); } }
-
翻转字符串
public class demo04 { public static void main(String[] args) { String s = "a54s5d"; System.out.println(f4(s, s.length()-1)); } // 翻转字符串 public static String f4(String s, int end) { if (end == 0) { return "" + s.charAt(0); } return s.charAt(end)+f4(s, end-1); } }
基础题(重复中的变化,变化中的重复)
递归:
上面的例子是 直接量 + 小规模子问题
下面的例子是多个小规模子问题
● 斐波那契数列
找递推公式 f(n) = f(n-1) + f(n-2)
public class demo05 {
// 测试
public static void main(String[] args) {
System.out.println(fib(3));
}
// 斐波那契序列
public static int fib(int n) {
if(n == 1 || n == 2)
return 1;
return fib(n-1) + fib(n-2); // 递归树,当n较大时,规模很大
}
}
● 最大公约数
辗转相除法 m % n = k
若 k = 0,则 n 为最大公约数
若 k ≠ 0,则 n % k = k` (重复如此,直到余数为 0 )
public class demo03 {
// 测试
public static void main(String[] args) {
System.out.println(gcd(15, 9));
}
// 最大公约数
public static int gcd(int m, int n) {
if (n == 0) {
return m;
}
return gcd(n, m % n);
}
}
● 递归进行插入排序
插入排序就是将数组看成了是 有序表 + 无序表
两部分,每次从无序表中取出一个数在有序表中排序。
public class demo04 {
public static void main(String[] args) {
int[] arr = new int[]{5,8,3,4,1};
insertSort(arr, arr.length-1);
for (int i : arr) {
System.out.print(i+" ");
}
}
public 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--];
}
arr[index+1] = x;
}
}
● 汉诺塔🔺
1 ~ N 从 A 移动到 B,C 作为辅助
等价于:
- 把 1 ~ N-1 从 A 移动到 C,B 作为辅助
- 把 N 从 A 移动到 B
- 把 1 ~ N-1 从 C 移动到 B,A 作为辅助
// 汉诺塔递归解法
public class demo05 {
public static void main(String[] args) {
HanoiTower(3, "A", "B", "C");
}
/**
* 将N个盘子从source移动到target的路径的打印
* @param N 初始的N个从小到达的盘子,N是最大编号
* @param from 原始柱子
* @param to 目标柱子
* @param help 辅助柱子
*/
public static void HanoiTower(int N, String from, String to, String help){
if(N == 1) {
System.out.println("move " + N + " from " + from + " to " + to);
return;
}
// 把 1 ~ N-1 挪到辅助柱子上
HanoiTower(N - 1, from, help, to);
// N 顺利到达target
System.out.println("move " + N + " from " + from + " to " + to);
// 把 1 ~ N-1 从辅助柱子挪到目标柱子上
HanoiTower(N - 1, help, to, from);
}
}
2. 查找
二分查找
public class binarySearch {
public static int BinarySearch(int[] arr, int low, int high, int target) {
if(low > high)
return -1;
int mid = low + ((high- low) >> 1);
// int mid = (low + high) >>> 1; 防止溢出,移位也高效
if(arr[mid] > target)
return BinarySearch(arr, low, mid-1, target);
else if(arr[mid] < target)
return BinarySearch(arr, mid+1, high, target);
else
return mid;
}
// 测试
public static void main(String[] args) {
int[] arr = new int[] {1,5,8,10,13,21,55};
System.out.println(BinarySearch(arr, 0, arr.length-1, 13));
}
}
3. 排序
冒泡排序
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定性:相同元素排序前和排序后相对位置不会变化,即稳定
import java.util.Arrays;
public class bubbleSort {
public static void BubbleSort1(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
for(int i = 0; i < arr.length - 1; i++) { // 循环轮数,即第i轮冒泡
for(int j = 0; j < arr.length - 1 - i; j++) { // 两两比较排序
if(arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
// 改进
public static void BubbleSort2(int[] arr) {
for(int i = 0; i < arr.length - 1; i++) {
// 标记这一趟是不是有交换,如果没有交换,顺序就已经排好了
// 每一轮开始均假设已经排好序
boolean flag = true;
for(int j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = false; // 有交换表示本轮并没有排好序
}
}
// 若并没有进行交换,即已经排好序了,不用再进行下一轮冒泡
if(flag) {
break;
}
}
}
// 测试
public static void main(String[] args) {
int[] array1 = new int[]{55, 33, 22, 66, 11};
System.out.print("排序前:");
System.out.println(Arrays.toString(array1));
// 调用BubbleSort1方法对array数组进行排序
BubbleSort1(array1);
System.out.print("BubbleSort1排序后:");
System.out.println(Arrays.toString(array1));
// 调用改进的BubbleSort2方法对array数组进行排序
int[] array2 = new int[]{55, 33, 22, 66, 11};
BubbleSort2(array2);
System.out.print("BubbleSort2排序后:");
System.out.println(Arrays.toString(array2));
// Array.sort()
int[] array3 = new int[]{55, 33, 22, 66, 11};
Arrays.sort(array3);
System.out.print("Arrays.sort排序后:");
System.out.println(Arrays.toString(array3));
// 获取时间可以用long now = System.currentTimeMillis();
}
}
选择排序
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定性:不稳定
import java.util.Arrays;
public class selectionSort {
// 选择排序
public static void SelectionSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
// 每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素,直到所有元素排完为止
for(int i = 0; i < arr.length - 1; i++) {
int min = i;
for(int j = i + 1; j < arr.length; j++) {
if(arr[min] > arr[j])
min = j;
}
if(min != i)
swap(arr, i, min);
}
}
// 交换
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
// 测试
public static void main(String[] args) {
int[] arr = {4, 5, 6, 3, 2, 1};
System.out.print("排序前:");
System.out.println(Arrays.toString(arr));
SelectionSort(arr);
System.out.print("SelectionSort排序后:");
System.out.println(Arrays.toString(arr));
int[] arr2 = {4, 5, 6, 3, 2, 1};
Arrays.sort(arr2);
System.out.print("Arrays.sort排序后:");
System.out.println(Arrays.toString(arr2));
}
}
插入排序
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
- 稳定性:稳定
import java.util.Arrays;
public class insertSort {
// 插入排序
public static void InsertSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
for(int i = 1; i < arr.length; i++) { // 第一个元素已经有序,从第二个开始
int temp = arr[i]; // 要进行插入的数
int j = i - 1; // 向前扫描已排序部分进行插入
while(j >= 0 && arr[j] > temp) {
arr[j + 1] = arr[j]; // temp比arr[j]小,将arr[j]后移
j--;
}
arr[j + 1] = temp;
}
}
// 测试
public static void main(String[] args){
int[] arr = {51,2,35,64,55,14,34,28,9};
System.out.print("排序前:");
System.out.println(Arrays.toString(arr));
InsertSort(arr);
System.out.print("SelectionSort排序后:");
System.out.println(Arrays.toString(arr));
int[] arr2 = {51,2,35,64,55,14,34,28,9};
Arrays.sort(arr2);
System.out.print("Arrays.sort排序后:");
System.out.println(Arrays.toString(arr2));
}
}
希尔排序
- 时间复杂度:在O(nlogn)~O(n²)之间
- 空间复杂度:O(1)
- 稳定性:不稳定
import java.util.Arrays;
public class shellSort {
// 希尔排序
public static void ShellSort(int[] arr) {
if(arr == null || arr.length <= 1)
return;
// 外层循环控制增量的变化
for(int interval = arr.length/2; interval > 0; interval/=2) {
// 内层循环进行增量为interval的插入排序
for(int i = interval; i < arr.length; i++) {
int temp = arr[i];
int idx = i - interval;
while(idx >= 0 && arr[idx] > temp) {
arr[idx + interval] = arr[idx];
idx -= interval;
}
arr[idx + interval] = temp;
}
}
}
// 测试
public static void main(String[] args){
int[] arr = {51,2,35,64,55,14,34,28,9};
System.out.print("排序前:");
System.out.println(Arrays.toString(arr));
ShellSort(arr);
System.out.print("SelectionSort排序后:");
System.out.println(Arrays.toString(arr));
int[] arr2 = {51,2,35,64,55,14,34,28,9};
Arrays.sort(arr2);
System.out.print("Arrays.sort排序后:");
System.out.println(Arrays.toString(arr2));
}
}
快速排序(分治)
快排——>划分
jdk中的 Array.sorts() 是用的快排算法
● 一遍单向扫描法
思路:
用两个指针将数组:
- 扫描指针(scan_pos) ——sp,从左往右,确认值 小于等于 主元;
- 右侧这阵——rp,从右区间向左,确认值 大于 主元的元素;
划分为三个区间:
- sp扫过的区间为 小于等于 主元的区间;
- rp扫过的区间为 大于 主元的区间;
- sp 与 rp 之间的区间未 未知 的。
import java.util.Arrays;
public class quickSort {
public static void QuickSort1(int[] arr, int l, int r) {
if(l < r) {
int q = Partition(arr, l, r);
QuickSort1(arr, l, q - 1);
QuickSort1(arr, q + 1, r);
}
}
public static int Partition(int[] arr, int p, int r) {
int pivot = arr[p];
int sp = p + 1; // 扫描指针
int rp = r; // 右侧指针
while(sp <= rp) {
if(arr[sp] <= pivot)
sp++;
else {
swap(arr, sp, rp);
rp--;
}
}
swap(arr, p, rp);
return rp;
}
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
// 测试
public static void main(String[] args){
int[] arr = {51,2,35,64,55,14,34,28,9};
System.out.print("排序前:");
System.out.println(Arrays.toString(arr));
System.out.print("QuickSort1一遍单向扫描法排序后:");
QuickSort1(arr, 0, 8);
System.out.println(Arrays.toString(arr));
int[] arr2 = {51,2,35,64,55,14,34,28,9};
Arrays.sort(arr2);
System.out.print("Arrays.sort排序后:");
System.out.println(Arrays.toString(arr2));
}
}
● 双向扫描分区法
思路:
头尾指针往中间扫描,从左找到 大于 主元的元素,从右找到 小于等于 主元的元素二者交换,继续扫描,直到左侧无大元素,右侧无小元素。
修改Partition
即可:
public static int Partition2(int[] arr, int p, int r) {
// 优化,在p,r,mid之间,选一个中间值作为主元
int mid = p + ((r - p) >> 1); // 数组中间的下标
int midValIdx = -1; // 三者中是中值的下标
if((arr[p] >= arr[mid] && arr[p] <= arr[r]) || (arr[p] <= arr[mid] && arr[p] >= arr[r]))
midValIdx = p;
else if((arr[r] >= arr[mid] && arr[r] <= arr[p]) || (arr[r] <= arr[mid] && arr[r] >= arr[p]))
midValIdx = r;
else
midValIdx = mid;
swap(arr, p, midValIdx);
int pivot = arr[p];
int lp = p + 1; // 左侧指针
int rp = r; // 右侧
while (lp <= rp) {
while (lp <= rp && arr[lp] <= pivot) lp++;
while (lp <= rp && arr[rp] > pivot) rp--;
if(lp < rp)
swap(arr, lp, rp);
}
swap(arr, p, rp);
return rp;
}
● 三指针分区法
主元出现较多重复数时可用
思路:
在单向扫描的基础上增加一个指针 ep,指向主元相等的区域。
ep指向第一个等于pivot的元素后,sp指向元素:
- 小于主元,sp与ep进行交换,ep++,sp++;
- 等于主元,sp++;
- 大于主元,sp与rp进行交换,rp–。
归并排序(分治)
归并——>合并
思路:将数组分为左右两个子数组,递归调用归并进行排序,排序后使用辅助的合并函数将两个有序的子数组合并成一个整体有序的数组
- 时间复杂度:O(nlogn)
- 空间复杂度:需要开辟辅助空间,该辅助空间可以重用,大小为数组长度N
- 稳定性:稳定
import java.util.Arrays;
public class mergeSort {
private static int[] helper;
public static void MergeSort(int[] arr) {
// 开辟辅助空间
helper = new int[arr.length];
MergeSort(arr, 0, arr.length - 1);
}
public static void MergeSort(int[] arr, int p, int r) {
if(p < r) {
int mid = p + ((r - p) >> 1);
MergeSort(arr, p, mid); // 左侧排序
MergeSort(arr, mid + 1, r); // 右侧排序
merge(arr, p, mid, r); // 合并
}
}
/**
* @param arr 原数组
* @param p 低位
* @param mid 中间位
* @param r 高位
*/
public static void merge(int[] arr, int p, int mid, int r) {
// 将原数组拷贝到helper中相应位置
System.arraycopy(arr, p, helper, p, r-p+1);
// 三个指针
int left = p, right = mid + 1;
int current = p;
// 比较合并
while(left <= mid && right <= r) {
if(helper[left] <= helper[right]) {
arr[current++] = helper[left++];
}else {
arr[current++] = helper[right++];
}
}
// 若左侧排完,右侧还有未比较的,不用进行处理(已在arr中相应位置)
// 若右侧排完,左侧还有未比较的,要把左侧剩下的搬到arr中
while(left <= mid) {
arr[current++] = helper[left++];
}
}
// 测试
public static void main(String[] args){
int[] arr = {51,2,35,64,55,14,34,28,9};
System.out.print("排序前:");
System.out.println(Arrays.toString(arr));
System.out.print("MergeSort排序后:");
MergeSort(arr);
System.out.println(Arrays.toString(arr));
int[] arr2 = {51,2,35,64,55,14,34,28,9};
Arrays.sort(arr2);
System.out.print("Arrays.sort排序后:");
System.out.println(Arrays.toString(arr2));
}
}
堆排序
-
二叉堆
- 父结点的键值总是≥(≤)任何一个子结点的键值
- 每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
-
任意结点的值都大于其子节点的值——大顶堆
任意结点的值都小于其子节点的值——小顶堆
-
堆排序
-
堆化,反向调整使得每个子数都是大顶堆或小顶堆
从
n/2-1
个元素开始向下修复,将每个节点修复为小(大)顶堆 -
按序输出元素:把堆顶和最末元素对调,然后调整堆顶元素。
小顶堆可以对数组逆序排序,每次交换栈顶和末尾元素,对栈顶进行向下修复,这样次小元素又到堆顶了
-
-
小顶堆
// 堆化
public static void makeMinHeap(int[] arr) { // 小顶堆
int n = arr.length;
for(int i = n/2-1; i >= 0; i--) {
minHeapFixDown(arr, i, n);
}
}
// 生成小顶堆
public static void minHeapFixDown(int[] arr, int i, int n) {
// i结点的左右孩子
int left = 2 * i + 1;
int right = 2 * i + 2;
// 判断是否越界
if(left >= n) return; // 左孩子越界,即不存在左孩子,i为叶子结点
int min = left;
if(right >= n) { // 若右孩子越界,即无右孩子
min = left;
}else { // 若左右孩子都存在
if(arr[left] > arr[right])
min = right;
}
// min指向了左右孩子中较小的那个
// 判断i结点与左右孩子结点的大小关系
if(arr[i] <= arr[min]) return; // 若i小于孩子们,不做交换
// 若i大于孩子们,与更小的孩子进行交换
swap(arr,i,min);
// 对交换的孩子结点进行小顶堆的检查调整
minHeapFixDown(arr, min, n);
}
public static void sort(int[] arr) {
// 堆化
makeMinHeap(arr);
// 排序
for(int x = arr.length - 1; x >= 0; x--) {
// 把堆顶(0号元素)和最后一个元素进行对调
swap(arr, 0, x);
// 缩小堆的范围(每次-1),对堆顶元素进行最小堆调整
minHeapFixDown(arr, 0, x);
}
}
- 大顶堆
// 大顶堆
public static void makeMaxHeap(int[] arr) {
int n = arr.length;
for(int i = n/2-1; i >= 0; i--) {
MaxHeapFixDown(arr, i, n);
}
}
public static void MaxHeapFixDown(int[] arr, int i, int n) {
// 左右孩子
int left = i * 2 + 1;
int right = i * 2 + 2;
// 是否越界
if(left >= n) return;
int max = left;
if(right >= n) {
max = left;
}else {
if(arr[left] < arr[right])
max = right;
}
// max指向左右孩子中较大的那个
// i结点与孩子结点进行判断
if(arr[i] >= arr[max]) return;
swap(arr, i, max);
// 对交换的结点进行检查调整
MaxHeapFixDown(arr, max, n);
}
public static void sort2(int[] arr) {
// 堆化
makeMaxHeap(arr);
// 排序
for(int x = arr.length-1; x >= 0; x--) {
swap(arr, 0, x);
MaxHeapFixDown(arr, 0, x);
}
}
测试用:
public static void main(String[] args) {
int[] arr = {36,6,34,11,81,18,61,47,99,7};
System.out.print("排序前:");
System.out.println(Arrays.toString(arr));
System.out.print("堆排序后(逆序):");
sort1(arr);
System.out.println(Arrays.toString(arr));
System.out.print("堆排序后(正序):");
sort2(arr);
System.out.println(Arrays.toString(arr));
}
计数排序
思路:
开辟新的空间,空间大小为max(source),扫描source,将value作为辅助空间的下标,用辅助空间的改位置元素记录value的个数。
之后,遍历helper就能将source修复为升序排列:
-
如果该位(index)的值为0,说明index不曾在source中出现;
-
如果该位(index)的值为1,说明index在source中出现了1次,为2自然是出现了2次。
时间复杂度: 扫描一次source,扫描一次helper,复杂度为N+k;
空间复杂度:辅助空间k,k=maxOf(source)。
存在问题:
-
重复元素
// 计数排序(原数组有重复值) public static void sort(int[] source) { int[] helper = new int[maxOf(source) + 1]; for(int e : source) { helper[e]++; } int current = 0; //数据回填的位置 for(int i = 0; i < helper.length; i++) { while(helper[i] > 0) { source[current++] = i; helper[i]--; } } }
-
有负数
找出原数组的max值和min值,辅助数组的空间大小为
max-min+1
,以source[i]-min
作为下标。// 计数排序(原数组有负数) public static void CountSort(int[] source) { int min = minOf(source); int max = maxOf(source); int[] helper = new int[max - min + 1]; for(int e : source) { helper[e - min]++; } int current = 0; for(int i = 0; i < helper.length; i++) { while (helper[i] > 0) { source[current++] = i + min; helper[i]--; } } }
测试:
// 数组最大值
public static int maxOf(int[] arr) {
int max = 0;
for(int i = 1; i < arr.length; i++) {
if(arr[i] > arr[max])
max = i;
}
return arr[max];
}
// 数组最小值
public static int minOf(int[] arr) {
int min = 0;
for(int i = 1; i < arr.length; i++) {
if(arr[i] < arr[min])
min = i;
}
return arr[min];
}
// 测试
public static void main(String[] args){
int[] arr = {51,2,2,55,2,14,34,28,9};
System.out.print("排序前:");
System.out.println(Arrays.toString(arr));
System.out.print("计数排序后(数组值可重复,不可为负数):");
sort(arr);
System.out.println(Arrays.toString(arr));
System.out.println("=====================");
int[] arr2 = {51,2,2,55,2,14,34,28,-9};
System.out.print("排序前:");
System.out.println(Arrays.toString(arr2));
System.out.print("计数排序后(数组值可重复,也可为负数):");
CountSort(arr2);
System.out.println(Arrays.toString(arr2));
}
桶排序
- “分配” & “收集”
- 思路:设计k个桶 (bucket)(编号0 ~ k-1),然后将n个输入数分布到各个桶中去,对各个桶中的数进行排序,然后按照次序把各个桶中的元素列出来即可。
基数排序
- “分配” & “收集”
- 思路:对于十进制数来说,每一位的数在[0,9]之中,d位的数,则有d列。基数排序先按低位有效数字进行排序,然后逐次向上一位进行排序,直到最高位排序结束。
- 问题:待排数字中没有0,没有负数
import java.util.ArrayList;
import java.util.Arrays;
public class radixSort {
// 生成10个桶,每个桶装的个数不定:用数组+ArrayList
private static ArrayList[] bucket = new ArrayList[10];
// 初始化桶(静态块初始化桶,在类生成时即执行)
static {
for(int i = 0; i < bucket.length; i++) {
bucket[i] = new ArrayList();
}
}
// 每轮排序,arr为原数组,d为位数
public static void sort(int[] arr, int d) {
// 入桶
for(int i = 0; i < arr.length; i++) {
putInBucket(arr[i], getDigitOn(arr[i], d));
}
// 出桶
int current = 0;
for(int j = 0; j < bucket.length; j++) {
for(Object o : bucket[j]) {
arr[current++] = (Integer) o;
}
}
// 清空桶
clearAll();
}
// 入桶
public static void putInBucket(int data, int digitOn) {
switch (digitOn) {
case 0:
bucket[0].add(data);
break;
case 1:
bucket[1].add(data);
break;
case 2:
bucket[2].add(data);
break;
case 3:
bucket[3].add(data);
break;
case 4:
bucket[4].add(data);
break;
case 5:
bucket[5].add(data);
break;
case 6:
bucket[6].add(data);
break;
case 7:
bucket[7].add(data);
break;
case 8:
bucket[8].add(data);
break;
default:
bucket[9].add(data);
break;
}
}
// 清空
public static void clearAll() {
for(ArrayList b : bucket) {
b.clear();
}
}
// 排序
public static void radixSort(int[] arr) {
int d = 1; // 入桶的位初始化为1,即最低位
int max = maxOf(arr); // 数组最大值
// 最大值的位数
int dNum = 1;
while(max / 10 != 0) {
dNum++;
max /= 10;
}
// 根据位数循环进行入桶出桶
while (d <= dNum) {
sort(arr, d++);
}
}
// 数组最大值
public static int maxOf(int[] arr) {
int max = 0;
for(int i = 1; i < arr.length; i++) {
if(arr[i] > arr[max])
max = i;
}
return arr[max];
}
// 数字的第i位(由低位数起)
public static int getDigitOn(int x, int i) {
int e = 0;
while(i >= 1) {
e = x % 10;
x /= 10;
i--;
}
return e;
}
// 测试
public static void main(String[] args){
int[] arr = {51,2,2,55,2,14,34,28,9};
System.out.print("排序前:");
System.out.println(Arrays.toString(arr));
System.out.print("基数排序:");
radixSort(arr);
System.out.println(Arrays.toString(arr));
}
}