算法排序:递归、回溯、数论、插入、希尔、归并、选择、冒泡、快排、贪心、动态规划
数论思想:利用数学公式或者定理或者规律求解问题;
算法思想中最难的点:递归+动态规划;树论:二叉树,红黑树
思考题:
-
微信分销系统中有一个返利,大家应该都知道,比如B是A的下线,C是B的下线,那么在分钱返利的时候A可以分B,C的钱,这时候我们是不是就要分别找B,C的最后上级。这个问题我们一般怎么来解决呢?
-
斐波那契数列:1 1 2 3 5 8 13 21 有什么特点?从第三个数开始 就等于前面两个数相加; 求解公式:f(n)=f(n-1)+f(n-2) 终止条件:n<=2 f(n)=1
递归
递归的定义
比如我们在某窗口排队人太多了,我不知道我排在第几个,那么我就问我前面的人排第几个,
因为知道他排第几我就知道我是第几了。但前面的人也不知道自己排第几那怎么办呢?他也可以继续往前面问,直到问到第一个人,然后从第一个人一直传到我这里 我就很清楚的知道我是第几了。以上这个场景就是一个典型的递归。我们在这个过程中大家有没有发现一个规律那么就是会有一个问的过程,问到第一个后有一个回来的过程吧。这就是递(问)加归(回)。那么这个过程我们是不是可以用一个数学公式来求解呢?那这个数学公式又是什么?
f(n)=f(n-1)+1
f(n):表示我的位置
f(n-1):表示我前面的那个人;
自己调用自己;
递归应用场景
-
一个问题的解可以分解为几个子问题的解
:子问题,我们通过分治的思想可以把一个数据规模大的问题,分解为很多小的问题。
我们可以把刚刚那个问前面的那个人看为子问题。大化小 -
这个问题与分解之后的子问题,
求解思路完全一样
-
一定有一个最后确定的答案,即递归的终止条件
:刚刚那个问题就是第一个人。第一个人是肯定知道自己排第几吧即n=1的时候,如果没有这个特性那么我们这个递归就会出现死循环,最后程序就是栈溢出;stack out of
递归的时间和空间复杂度分析
以斐波那契数列为例为分析递归树:f(n)=f(n-1)+f(n-2)
时间复杂度和空间复杂度都是:O(2^n)=>O(n)或者O(nlogn)
递归优化
- 使用非递归: 所有的递归代码理论上是一定可以转换成非递归的
- 加入缓存:把我们中间的运算结果保存起来,这样就可以把递归降至为o(n)
- 尾递归:什么是尾递归?尾递归就是调用函数一定出现在末尾,没有任何其他的操作了。因为我们编译器在编译代码时,如果发现函数末尾已经没有操作了,这时候就不会创建新的栈,而且覆盖到前面去。倒着算,不需要在回溯了,因为我们每次会把中间结果带下去。
// 斐波纳契/微信分销系统
// 1 1 2 3 5 8 13
// f(n) = f(n-1) + f(n-2)
// 递归
// 递归优化
// 1. 使用非递归
// 2. 加入缓存
// 3. 尾递归
// 递归注意事项----栈溢出和时间问题
public class FibonacciSeq {
//递归 40 : 102334155 耗时:316 ms
public static int fab(int n) {
// 时间复杂度/空间复杂度 O(2^n) => 如何优化
if (n <= 2) return 1;
return fab(n - 1) + fab(n - 2);
}
//尾递 第一个优化 40 : 102334155 耗时:0 ms
public static int noFab(int n) {
// 不使用递归-节约空间
if (n <= 2) return 1;
int a = 1;
int b = 1;
int c = 0;
for (int i = 3; i <= n; i ++) {
c = a + b;
a = b;
b = c;
}
return c;
}
//尾递 40 : 102334155 耗时:0 ms
public static int noFabSimple(int data[], int n) {
// 不使用递归--占用空间
if (n <= 2) return 1;
data[1] = 1;
data[2] = 1;
for (int i = 3; i <= n; i ++) {
data[i] = data[i - 1] + data[i - 2];
}
return data[n];
}
//尾递 40 : 102334155 耗时:0 ms
public static int fab2(int data[], int n) {
if (n <= 2) return 1;
if (data[n] > 0) return data[n];
int res = fab2(data,n-1) + fab2(data,n-2);
data[n] = res;
return res;
}
//尾递: 改成尾递归 prepre 上上一次运行结果 pre 上次运行结果
public static int tailFab2(int n, int prepre, int pre) {
if (n <= 2) return pre;
return tailFab2(n - 1, pre, pre + prepre);
}
//求N的阶乘 用普通的递归怎么写 5=5*4*3*2*1 => f(n) = n * f(n-1)
public static int fac(int n) {
if (n <= 1) return 1;
return n * fac(n - 1);
}
//改成尾递归 求N的阶乘 用普通的递归怎么写 5=5*4*3*2*1 => f(n) = n * f(n-1)
public static int tailFac(int n, int res) {
//尾递归
if (n <= 1) return res;
return tailFac(n-1, n * res);
}
public static void main(String[] args) {
// for (int i = 1; i <= 40; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + fab(i) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
// for (int i = 1; i <= 40; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + noFab(i) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
// data = new int[41];
// for (int i = 1; i <= 40; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + noFabSimple(data, i) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
// data = new int[41];
// for (int i = 1; i <= 40; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + fab2(data, i) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
// for (int i = 1; i <= 40; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + tailFab2(i, 1, 1) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
//
// for (int i = 1; i <= 10; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + tailFac(i, 1) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
// for (int i = 1; i <= 10; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + fac(i) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
}
}
/**
递归
1 : 1 耗时:0 ms
2 : 1 耗时:0 ms
3 : 2 耗时:0 ms
4 : 3 耗时:0 ms
5 : 5 耗时:0 ms
6 : 8 耗时:0 ms
7 : 13 耗时:0 ms
8 : 21 耗时:0 ms
9 : 34 耗时:0 ms
10 : 55 耗时:0 ms
11 : 89 耗时:0 ms
12 : 144 耗时:0 ms
13 : 233 耗时:0 ms
14 : 377 耗时:0 ms
15 : 610 耗时:0 ms
16 : 987 耗时:1 ms
17 : 1597 耗时:0 ms
18 : 2584 耗时:0 ms
19 : 4181 耗时:0 ms
20 : 6765 耗时:0 ms
21 : 10946 耗时:0 ms
22 : 17711 耗时:0 ms
23 : 28657 耗时:1 ms
24 : 46368 耗时:0 ms
25 : 75025 耗时:0 ms
26 : 121393 耗时:1 ms
27 : 196418 耗时:1 ms
28 : 317811 耗时:1 ms
29 : 514229 耗时:1 ms
30 : 832040 耗时:4 ms
31 : 1346269 耗时:4 ms
32 : 2178309 耗时:7 ms
33 : 3524578 耗时:11 ms
34 : 5702887 耗时:17 ms
35 : 9227465 耗时:30 ms
36 : 14930352 耗时:50 ms
37 : 24157817 耗时:90 ms
38 : 39088169 耗时:145 ms
39 : 63245986 耗时:242 ms
40 : 102334155 耗时:316 ms
不使用递归
0 : 1 耗时:0 ms
1 : 1 耗时:0 ms
2 : 1 耗时:0 ms
3 : 2 耗时:0 ms
4 : 3 耗时:0 ms
5 : 5 耗时:0 ms
6 : 8 耗时:0 ms
7 : 13 耗时:0 ms
8 : 21 耗时:0 ms
9 : 34 耗时:0 ms
10 : 55 耗时:0 ms
11 : 89 耗时:0 ms
12 : 144 耗时:0 ms
13 : 233 耗时:0 ms
14 : 377 耗时:0 ms
15 : 610 耗时:0 ms
16 : 987 耗时:0 ms
17 : 1597 耗时:0 ms
18 : 2584 耗时:0 ms
19 : 4181 耗时:0 ms
20 : 6765 耗时:0 ms
21 : 10946 耗时:0 ms
22 : 17711 耗时:0 ms
23 : 28657 耗时:0 ms
24 : 46368 耗时:0 ms
25 : 75025 耗时:0 ms
26 : 121393 耗时:0 ms
27 : 196418 耗时:0 ms
28 : 317811 耗时:0 ms
29 : 514229 耗时:0 ms
30 : 832040 耗时:0 ms
31 : 1346269 耗时:0 ms
32 : 2178309 耗时:0 ms
33 : 3524578 耗时:0 ms
34 : 5702887 耗时:0 ms
35 : 9227465 耗时:0 ms
36 : 14930352 耗时:0 ms
37 : 24157817 耗时:0 ms
38 : 39088169 耗时:0 ms
39 : 63245986 耗时:0 ms
40 : 102334155 耗时:0 ms
*/
排序性能分析
- 时间效率:决定了算法运行多久,O(1)
- 空间复杂度
- 比较次数&交换次数
- 稳定性
1 9 *3 5 3
第一种:1 *3 3 5 9
第二种:1 3 *3 5 9
相同的两个数排完序后,相对位置不变。稳定排序有什么意义?应用在哪里呢? 例如电商订单排序(从小到大排,金额相同的按下单时间)
插入排序
看以下这个例子:对7 8 9 0 4 3进行插入排序
7 8 9 0 4 3
7 8 9 0 4 3
7 8 9 0 4 3
0 7 8 9 4 3
0 4 7 8 9 3
0 3 4 7 8 9
public static int[] insertSort(int arr[]) {
for (int i = 1; i < arr.length; i++) {
int curr = arr[i];
int pre = i - 1;
for (; pre >= 0 ; pre--) {
if (curr < arr[pre]) {
arr[pre+1] = arr[pre];
} else {
break;
}
}
arr[pre+1] = curr;
}
return arr;
}
希尔排序
增量分段:add=n/2 n=10 =>5,2,1
public static void shellSort(int data[]) {
int n = data.length;
for (int add = n/2; add >= 1 ; add/=2) {
for (int i = add; i < n; i++) {
int temp = data[i];
int j = i - add;
for (; j >= 0; j-=add) {
if (data[j] > temp) {
data[j + add] = data[j];
} else {
break;
}
}
data[j+add] = temp;
}
}
}
归并排序
public static void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = (left + right)/2;
mergeSort(arr, left, mid);
mergeSort(arr, mid+1, right);
merge(arr, left, mid, right);
}
}
public static void merge(int[] arr, int left, int mid, int right) {
int temp[] = new int[arr.length];
int leftPoint = left;
int rightPoint = mid + 1;
int cur = left; //当前位置
while (leftPoint <= mid && rightPoint <= right) {
if (arr[leftPoint] < arr[rightPoint]) {
temp[cur] = arr[leftPoint];
leftPoint++;
} else {
temp[cur] = arr[rightPoint];
rightPoint++;
}
cur++;
}
while (leftPoint <= mid) {
temp[cur++] = arr[leftPoint++];
}
while (rightPoint <= right) {
temp[cur++] = arr[rightPoint++];
}
for (int i = left; i <= right; i++) {
arr[i] = temp[i];
}
}
选择
选择排序的思路和插入排序非常相似,也分已排序和未排序区间。但选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。但是不像插入排序会移动数组 选择排序会每次进行交换
public static int[] selectSort(int data[]) {
for (int i = 0; i < data.length -1; i++) {
int minloc = i;
for (int j = i+1; j < data.length; j++) {
if (data[j] < data[minloc]) {
minloc = j;
}
}
int minTemp = data[minloc];
data[minloc] = data[i];
data[i] = minTemp;
}
return data;
}
冒泡
核心思路:冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。
第一次冒泡的结果:4 5 6 3 2 1->4 5 3 6 2 1 - > 4 5 3 2 6 1 -> 4 5 3 2 1 6,哪个元素的位置确定了,6
第二次冒泡的结果:4 5 3 2 1 6->4 3 5 2 1 6 -> 4 3 2 5 1 6 -> 4 3 2 1 5 6
第三次冒泡的结果:4 3 2 1 5 6->3 4 2 1 5 6 -> 3 2 4 1 5 6 -> 3 2 1 4 5 6
第四次冒泡的结果:3 2 1 4 5 6->2 3 1 4 5 6 -> 2 1 3 4 5 6
第五次冒泡的结果:2 1 3 4 5 6->1 2 3 4 5 6
public static int[] dubbleSort(int data[]) {
for (int i = 0; i < data.length; i++) {
for (int j = i+1; j < data.length; j++) {
if (data[j] < data[i]) {
int temp = data[j];
data[j] = data[i];
data[i] = temp;
}
}
}
return data;
}
public static void dubbleSortTest(int arr[]) {
for (int i = 0; i < arr.length-1; i++) {
boolean flag = false;
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 = true;
}
}
if (!flag) break;
}
}
快排
快排和归并的对比:
- 归并排序的处理过程是由下到上的,先处理子问题,然后再合并。
- 快排其实就是从上到下,先分区,在处理子问题,不用合并。
其优化就是优化基准数,提供一个取三个数中间的思路.
45 28 80 90 50 16 100 10
基准数:一般就是取要排序序列的第一个。
第一次排序基准数:45
从后面往前找到比基准数小的数进行对换:
*10 28 80 90 50 16 100 *45
从前面往后面找比基准数大的进行对换:
10 28 *45 90 50 16 100 *80
。。。
以基准数分为3部分,左边的比之小,右边比之大:
{10 28 16} 45 {50 90 100 80}
到此第一次以45位基准数的排序完成。
public static void quickSort(int data[], int left, int right) {
int base = data[left]; //基准数
int ll = left; //从左边找的位置
int rr = right; //从右边找的位置
while (ll < rr) {
//从右边找比基数小的数
while (ll < rr && data[rr] >= base) {
rr--;
}
if (ll < rr) {
//表示有找到比之大的
int temp = data[rr];
data[rr] = data[ll];
data[ll] = temp;
ll++;
}
while (ll < rr && data[ll] <= base) {
ll++;
}
if (ll < rr) {
int temp = data[rr];
data[rr] = data[ll];
data[ll] = temp;
rr--;
}
}
if (left < ll)
quickSort(data, left, ll-1);
if (ll < right)
quickSort(data, ll+1, right);
}
对比
贪心算法
概念
概念:贪心算法又叫做贪婪算法,它在求解某个问题是,总是做出眼前最大利益。
也就是说只顾眼前不顾大局,所以它是局部最优解。核心点:通过局部最优推出全局最优
贪心算法的套路:一定会有一个排序。哈夫曼编码,贪心算法,压缩算法。最短路径
思考题
1.某天早上公司领导找你解决一个问题,明天公司有N个同等级的会议需要使用同一个会议室,现在给你这个N个会议的开始和结束
时间,你怎么样安排才能使会议室最大利用?即安排最多场次的会议?电影的话 那肯定是最多加票价最高的,入场率。综合算法
2.双十一马上就要来了,小C心目中的女神在购物车加了N个东西,突然她中了一个奖可以清空购物车5000元的东西(不能找零),每个东西只能买一件,那么她应该如何选择物品使之中奖的额度能最大利用呢?如果存在多种最优组合你只需要给出一种即可,嘿嘿 现在女神来问你,你该怎么办?
/**
* 贪心算法
* 最优
* 最短
* 最好
*
* 先按照开始时间排序, 之后按照当前开始时间,比较开始时间是否大于结束时间
*/
public class Meeting implements Comparable<Meeting> {
int meNum;
int startTime;
int endTime;
public Meeting(int meNum, int startTime, int endTime) {
super();
this.meNum = meNum;
this.startTime = startTime;
this.endTime = endTime;
}
@Override
public int compareTo(Meeting o) {
if (this.endTime > o.endTime) {
return 1;
}
return -1;
}
@Override
public String toString() {
return "GreedySort{" +
"meNum=" + meNum +
", startTime=" + startTime +
", endTime=" + endTime +
'}';
}
/**
* 4
* 0 9
* 8 10
* 10 12
* 8 20
* GreedySort{meNum=1, startTime=0, endTime=9}
* GreedySort{meNum=3, startTime=10, endTime=12}
*/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
List<Meeting> meetings = new ArrayList<>();
int n = scanner.nextInt(); //会议
for (int i = 0; i < n; i++) {
int start = scanner.nextInt();
int end = scanner.nextInt();
Meeting meeting = new Meeting(i+1, start, end);
meetings.add(meeting);
}
meetings.sort(null);
int curTime = 0; //当前时间,从一天的0点开始
for (int i = 0; i < n; i++) {
Meeting meeting = meetings.get(i);
if (meeting.startTime >= curTime) {
System.out.println(meeting.toString());
curTime = meeting.endTime;
}
}
}
}
动态规划
思考题–背包问题:小偷去某商店盗窃,背有一个背包,容量是50kg,现在有以下物品(物品不能切分,且只有一个),请问小偷应该怎么拿才能得到最大的价值?
物品 | 重量 | 价值 |
---|---|---|
物品1 | 10kg | 60元 |
物品2 | 20kg | 100元 |
物品3 | 40kg | 120元 |
5kg的袋子
物品 | 重量 | 价值 |
---|---|---|
物品1 | 1 | 6 |
物品2 | 2 | 10 |
物品3 | 4 | 12 |
把5kg的袋子,拆分成1kg,1kg这样子计算,里面的表格就表示当前重量下能装的最多的钱。表格的数列就表示是要装的物品
物品 | 1kg | 2kg | 3kg | 4kg | 5kg |
---|---|---|---|---|---|
加入物品1 | 6 | 6 | 6 | 6 | 6 |
加入物品2 | 6 | 10 | 10+6=16 | 10+6=16 | 16 |
加入物品3 | 6 | 10 | 16 | 16 | 18 |
/**
* 背包算法
* 购物车问题保存价值一样就可以
*/
public class Backpack {
public static List<Integer> group(int dp[][], int good_list[]) {
int value_max = dp[0].length - 1;
int good_max = dp.length - 1;
List<Integer> good_group = new ArrayList();
while (value_max > 0 && good_max > 0) {
if (dp[good_max][value_max] <= dp[good_max-1][value_max]) {
good_max -= 1;
} else {
good_group.add(good_max);
value_max -= good_list[good_max-1];
good_max -= 1;
}
}
return good_group;
}
public static int cart(int weight[], int lw) {
int n = weight.length;
int dp[][] = new int[n+1][lw+1]; //n表示物品、w表示重量,初始化全是0
for (int i = 1; i <= n; i++) {
//每次加的物品
for (int w = 1; w <= lw; w++) {
if (weight[i-1] <= w) {
//当前物品重量小于分割重量 表示这个物品可以装进去
dp[i][w] = Math.max(weight[i-1] + dp[i-1][w-weight[i-1]], dp[i-1][w]);
} else {
dp[i][w] = dp[i-1][w];
}
}
}
List<Integer> good_group = group(dp, weight);
System.out.print("组合:");
for (Integer integer : good_group) {
System.out.print(integer + "\t");
}
System.out.println();
return dp[n][lw];
}
public static int backpack(int value[], int weight[], int lw) {
int n = weight.length;
int dp[][] = new int[n+1][lw+1]; //n表示物品、w表示重量,初始化全是0
for (int i = 1; i <= n; i++) {
//每次加的物品
for (int w = 1; w <= lw; w++) {
if (weight[i-1] <= w) {
//当前物品重量小于分割重量 表示这个物品可以装进去
dp[i][w] = Math.max(value[i-1] + dp[i-1][w-weight[i-1]], dp[i-1][w]);
} else {
dp[i][w] = dp[i-1][w];
}
}
}
List<Integer> good_group = group(dp, weight);
System.out.print("组合:");
for (Integer integer : good_group) {
System.out.print(integer + "\t");
}
System.out.println();
return dp[n][lw];
}
/**
* 组合:4 3 1
* 8
* 组合:3 1
* 180
*/
public static void main(String[] args) {
System.out.println(cart(new int[]{
1,2,3,4,5,9},8));
System.out.println(backpack(new int[]{
60, 100, 120},new int[]{
10, 20, 30},40));
}
}