数据结构基础之《(24)—动态规划》

暴力递归到动态规划

一、什么暴力递归可以继续优化

1、有重复调用同一个子问题的解,这种递归可以优化

2、如果每一个子问题都是不同的解,无法优化也不用优化

二、暴力递归和动态规划的关系

1、某一个暴力递归,有解的重复调用,就可以把这个暴力递归优化成动态规划

2、任何动态规划问题,都一定对应着某一个有解的重复调用的暴力递归

3、但不是所有的暴力递归,都一定对应着动态规划

三、常见的4种尝试模型

1、从左往右的尝试模型

2、范围上的尝试模型

3、多样本位置全对应的尝试模型

4、寻找业务限制的尝试模型

5、题目 -> 找到某一种暴力递归的写法 -> 有重复解的可以改动态规划

四、如何找到某个问题的动态规划方法

1、方法
(1)设计暴力递归:重要原则+4种常见尝试模型!重点!
(2)分析有没有重复解:套路解决
(3)用记忆化搜索 -> 用严格表结构实现动态规划:套路解决
(4)看看能否继续优化:套路解决

五、暴力递归到动态规划的套路

1、方法
(1)你已经有了一个不违反原则的暴力递归,而且的确存在解的重复调用
(2)找到哪些参数的变化会影响返回值,对每一个列出变化范围
(3)参数间的所有的组合数量,意味着表大小
(4)记忆化搜索的方法就是傻缓存,非常容易得到
(5)规定好严格表的大小,分析位置的依赖顺序,然后从基础填写到最终解
(6)对于有枚举行为的决策过程,进一步优化

六、题目1

假设有排成一行的N个位置,记为1~N,N一定大于或等于2
开始时机器人在其中的M位置上(M一定是1~N中的一个)
如果机器人来到1位置,那么下一步只能往右来到2位置
如果机器人来到N位置,那么下一步只能往左来到N-1位置
如果机器人来到中间位置,那么下一步可以往左走或者往右走
规定机器人必须走K步,最终能来到P位置(P也是1~N中的一个)的方法有多少种?
给定四个参数N、M、K、P,返回方法数。

例子:
1 2 3 4 5 6 7
N=7
M=3
P=2
K=3
这里问题就是起点是3位置,走3步来到2位置。

package class13;

/**
 * 机器人走步数
 */
public class Code01_RobotWalk {

	/**
	 * 方式一
	 * @param N
	 * @param M
	 * @param K
	 * @param P
	 * @return
	 */
	public static int way1(int N, int M, int K, int P) {
		//参数无效直接返回0
		if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
			return 0;
		}
		//总共N个位置,从M点出发,还剩K步,返回最终能达到P的方法数
		return walk(N, M, K, P);
	}
	
	/**
	 * 
	 * @param N 位置为1~N,固定参数
	 * @param cur 当前在cur位置,可变参数
	 * @param rest 还剩rest步没有走,可变参数
	 * @param P 最终目标位置是P,固定参数
	 * @return 返回可走的方法数
	 */
	public static int walk(int N, int cur, int rest, int P) {
		// 如果没有剩余步数了,当前的cur位置就是最后的位置
		// 如果最后的位置停在P上,那么之前做的移动是有效的
		// 如果最后的位置没停在P上,那么之前做的一定是无效的
		if (rest == 0) {
			return cur == P ? 1 : 0;
		}
		// 如果还有rest步要走,而当前的cur位置在1位置上,那么当前这步只能从1走向2
		// 后续的过程就是,来到2位置上,还剩rest-1步要走
		if (cur == 1) {
			return walk(N, 2, rest - 1, P);
		}
		// 如果还有rest步要走,而当前的cur位置在N位置上,那么当前这步只能从N走向N-1
		// 后续的过程就是,来到N-1位置上,还剩rest-1步要走
		if (cur == N) {
			return walk(N, N - 1, rest - 1, P);
		}
		// 如果还有rest步要走,而当前的cur位置在中间位置,那么当前这步可以走向左,也可以走向右
		// 走向左之后,后续的过程就是,来到cur-1位置上,还剩rest-1步要走
		// 走向右之后,后续的过程就是,来到cur+1位置上,还剩rest-1步要走
		// 走向左、走向右是截然不同的方法,所以总方法数要都算上
		return walk(N, cur + 1, rest - 1, P) + walk(N, cur - 1, rest - 1, P);
	}
	
	/**
	 * 方式二
	 * @param N
	 * @param M
	 * @param K
	 * @param P
	 * @return
	 */
	public static int way2(int N, int M, int K, int P) {
		//参数无效直接返回0
		if (N < 2 || K < 1 || M < 1 || M > N || P < 1 || P > N) {
			return 0;
		}
		//dp表把1~N位置,剩余1~K步,所有的结果装下
		int[][] dp = new int[N + 1][K + 1];
		//初始化dp表
		for (int row = 0; row <= N; row++) {
			for (int col = 0; col <= K; col++) {
				dp[row][col] = -1;
			}
		}
		
		//总共N个位置,从M点出发,还剩K步,返回最终能达到P的方法数
		return walkCache(N, M, K, P, dp);
	}
	
	/**
	 * 会有重复计算的过程,增加缓存(其实就是动态规划-计划搜索)
	 * 我想把所有cur和rest的组合,返回的结果,加入到缓存里
	 * @param N
	 * @param cur
	 * @param rest
	 * @param P
	 * @return
	 */
	public static int walkCache(int N, int cur, int rest, int P, int[][] dp) {

		if (dp[cur][rest] != -1) {
			return dp[cur][rest];
		}
		
		if (rest == 0) {
			dp[cur][rest] = (cur == P ? 1 : 0);
			return dp[cur][rest];
		}

		if (cur == 1) {
			dp[cur][rest] = walkCache(N, 2, rest - 1, P, dp);
			return dp[cur][rest];
		}

		if (cur == N) {
			dp[cur][rest] = walkCache(N, N - 1, rest - 1, P, dp);
			return dp[cur][rest];
		}

		dp[cur][rest] = walkCache(N, cur + 1, rest - 1, P, dp) + walkCache(N, cur - 1, rest - 1, P, dp);
		return dp[cur][rest];
	}
	
	public static void main(String[] args) {
		int N = 7;
		int M = 3;
		int P = 2;
		int K = 3;
		System.out.println(way1(N,M,K,P));
		System.out.println("====================");
		System.out.println(way2(N,M,K,P));
	}
}

七、题目2

给定一个字符串str,给定一个字符串类型的数组arr。
arr里的每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来。
返回需要至少多少张贴纸可以完成这个任务。
例子:str="babac",arr={"ba","c","abcd"}
至少需要两张贴纸"ba"和"abcd",因为使用这两张贴纸,把每一个字符单独剪开,含有2个a、2个b、1个c。是可以拼出str的。所以返回2。

例:
str="babbac"
arr={"ab","bba","ac","cc"}

package class13;

import java.util.Arrays;
import java.util.HashMap;

/**
 * 贴纸问题
 */
public class Code02_StickersToSpellWord {

	/**
	 * 方式一
	 * @param stickers
	 * @param target
	 * @return
	 */
	public static int minStickers1(String[] stickers, String target) {
		// 贴纸的数量
		int n = stickers.length;
		// 每个贴纸都转化成26个字母的数组
		int[][] map = new int[n][26]; //map是永远不变的
		// 每个贴纸的词频map生成
		for (int i = 0; i < n; i++) {
			char[] str = stickers[i].toCharArray();
			for (char c : str) {
				map[i][c - 'a']++; // 1位置表达a,2位置表达2...
			}
		}
		
		// 缓存
		HashMap<String, Integer> dp = new HashMap<>();
		dp.put("", 0); // 初始化,空字符返回需要0张贴纸
		
		return process1(dp, map, target);
	}
	
	/**
	 * 
	 * @param dp 缓存,如果t已经算过了,直接返回dp中的值
	 * @param map 每个贴纸所含字符的词频统计
	 * @param rest 剩余的字符串
	 * @return 如果返回值-1,表示map中的贴纸怎么都无法满足剩余的rest
	 */
	public static int process1(HashMap<String, Integer> dp, int[][] map, String rest) {
		// 如果缓存中有,直接从dp表中拿到答案
		if (dp.containsKey(rest)) {
			return dp.get(rest);
		}
		
		// 以下就是正式的递归调用过程
		
		int ans = Integer.MAX_VALUE; // 初始化ans,是要返回的贴纸数量
		int n = map.length; // n是贴纸数量
		
		// 把target转成词频的形式
		int[] tmap = new int[26]; // tmap去替代rest
		char[] target = rest.toCharArray();
		for (char c : target) {
			tmap[c - 'a']++; // a剩几个,b剩几个,c剩几个...
		}
		
		for (int i = 0; i < n; i++) {
			// 枚举当前第一张贴纸是谁
			
			// 假设i是第一张贴纸
			if (map[i][target[0] - 'a'] == 0) { // 第i张贴纸的,target第一个字符位置词频是0,则遍历下一张贴纸
				// 说明i号贴纸不包含这个字符,用i+1号贴纸
				continue;
			}
			
			// sb是还差多少个字符
			StringBuilder sb = new StringBuilder();
			
			// i是i号贴纸,j是每一个字符的变化
			for (int j = 0; j < 26; j++) {
				if (tmap[j] > 0) { // j这个字符是target需要的
					for (int k = 0; k < Math.max(0, tmap[j] - map[i][j]); k++) {
						// target词频j位置的字符数,比map里第i张贴纸j位置的字符数要大,计算剩余的字符数
						sb.append((char)('a' + j));
					}
				}
			}
			
			// s是i贴纸搞定后剩余的字符
			String s = sb.toString();
			int tmp = process1(dp, map, s);
			if (tmp != -1) {
				// ans取,之前的ans和1加后续的贴纸数,取最小值
				ans = Math.min(ans, 1 + tmp);
			}
		}
		
		// 经历过for循环之后,发现ans没有被设置过,表示没有解,返回-1,否则存正常值
		dp.put(rest, ans == Integer.MAX_VALUE ? -1 : ans);
		// 从缓存中返回值
		return dp.get(rest);
	}
	
	/**
	 * 方式二
	 * @param stickers
	 * @param target
	 * @return
	 */
	public static int minStickers2(String[] stickers, String target) {
		int n = stickers.length;
		int[][] map = new int[n][26];
		for (int i = 0; i < n; i++) {
			char[] str = stickers[i].toCharArray();
			for (char c : str) {
				map[i][c - 'a']++;
			}
		}
		char[] str = target.toCharArray();
		int[] tmap = new int[26];
		for (char c : str) {
			tmap[c - 'a']++;
		}
		HashMap<String, Integer> dp = new HashMap<>();
		
		// 枚举每张贴纸,所有可能使用的张数,以后这张贴纸再也不碰
		// PS:方法一比方法二好,方法一只有1个可变参数,方法二有2个可变参数
		int ans = process2(map, 0, tmap, dp);
		return ans;
	}
	
	/**
	 * 
	 * @param map 每个贴纸所含字符的词频统计
	 * @param i 当前位置的贴纸用i张时
	 * @param tmap 把target转成词频的形式
	 * @param dp 缓存
	 * @return
	 */
	public static int process2(int[][] map, int i, int[] tmap, HashMap<String, Integer> dp) {
		StringBuilder keyBuilder = new StringBuilder();
		keyBuilder.append(i + "_");
		for (int asc = 0; asc < 26; asc++) {
			if (tmap[asc] != 0) {
				keyBuilder.append((char)(asc + 'a') + "_" + tmap[asc] + "_");
			}
		}
		String key = keyBuilder.toString();
		if (dp.containsKey(key)) {
			return dp.get(key);
		}
		boolean finish = true;
		for (int asc = 0; asc < 26; asc++) {
			if (tmap[asc] != 0) {
				finish = false;
				break;
			}
		}
		if (finish) {
			dp.put(key, 0);
			return 0;
		}
		if (i == map.length) {
			dp.put(key, -1);
			return -1;
		}
		int maxZhang = 0;
		for (int asc = 0; asc < 26; asc++) {
			if (map[i][asc] != 0 && tmap[asc] != 0) {
				maxZhang = Math.max(maxZhang, (tmap[asc] / map[i][asc]) + (tmap[asc] % map[i][asc]));
			}
		}
		int[] backup = Arrays.copyOf(tmap, tmap.length);
		int min = Integer.MAX_VALUE;
		int next = process2(map, i + 1, tmap, dp);
		tmap = Arrays.copyOf(backup, backup.length);
		if (next != -1) {
			min = next;
		}
		for (int zhang = 1; zhang <= maxZhang; zhang++) {
			for (int asc = 0; asc < 26; asc++) {
				tmap[asc] = Math.max(0, tmap[asc] - (map[i][asc] * zhang));
			}
			next = process2(map, i + 1, tmap, dp);
			tmap = Arrays.copyOf(backup, backup.length);
			if (next != -1) {
				min = Math.min(min, zhang + next);
			}
		}
		int ans = min == Integer.MAX_VALUE ? -1 : min;
		dp.put(key, ans);
		return ans;
	}
	
	public static void main(String[] args) {
		String[] arr = {"aaaa", "bbaa", "ccddd"};
		String str = "abcccccdddddbbbaaaaa";
		System.out.println(minStickers1(arr, str));
		System.out.println("====================");
		System.out.println(minStickers2(arr, str));
	}
	
}

八、题目3

两个字符串的最长公共子序列问题

例子:
字符串1:ab1cd2ef345gh
字符串2:opq123rs4tx5yz
最长公共子序列:12345

思路:
建一张表,str1做行,str2做列
函数f(str1, i1, str2, i2)

dp[i][j],str1从0出发到i位置和str2从0出发到j位置的最长公共子序列

第一行第一列,str1拿1个字符,str2拿1个字符,最长公共子序列是多少
第一行第二列,str1拿1个字符,str2拿2个字符,最长公共子序列是多少
...

最长公共子序列的最后一个字符在哪儿的分析:
str1[0...i]和str2[0...j]
(1)最大公共子序列,可能既不以str1[i]字符结尾,也不以str2[j]字符结尾
取值dp[i-1][j-1]
(2)最大公共子序列,可能以str1[i]字符结尾,不以str2[j]字符结尾
取值dp[i][j-1]
(3)最大公共子序列,不以str1[i]字符结尾,以str2[j]字符结尾
取值dp[i-1][j]
(4)最大公共子序列,既以str1[i]字符结尾,又以str2[j]字符结尾
取值dp[i-1][j-1]+1

package class13;

/**
 * 最长公共子序列问题
 */
public class Code05_lcse {

	public static int lcse(char[] str1, char[] str2) {
		
		//准备一个dp表
		int[][] dp = new int[str1.length][str2.length];
		
		//dp[0][0]位置
		dp[0][0] = str1[0] == str2[0] ? 1 : 0;
		
		for (int i = 1; i < str1.length; i++) {
			//填i行0列的所有值
			//一旦某个str1的字符等于str2[0],后面的都是1
			dp[i][0] = Math.max(dp[i - 1][0], str1[i] == str2[0] ? 1 : 0);
		}
		for (int j = 1; j < str2.length; j++) {
			//一旦某个str2的字符等于str1[0],后面的都是1
			dp[0][j] = Math.max(dp[0][j - 1], str1[0] == str2[j] ? 1 : 0);
		}
		for (int i = 1; i < str1.length; i++) {
			for (int j = 1; j < str2.length; j++) {
				dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
				if (str1[i] == str2[j]) {
					dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - 1] + 1);
				}
			}
		}
		return dp[str1.length - 1][str2.length -1];
	}
	
	public static void main(String[] args) {
		String str1 = "ab1cd2ef345gh";
		String str2 = "opq123rs4tx5yz";
		System.out.println(lcse(str1.toCharArray(), str2.toCharArray()));
	}
}

九、题目4

给定一个数组,代表每个人喝完咖啡准备刷杯子的时间
只有一台咖啡机,一次只能洗一个杯子,时间耗费a,洗完才能洗下一杯
每个咖啡杯也可以自己挥发干净,时间耗费b,咖啡杯可以并行挥发
返回让所有咖啡杯变干净的最早完成时间
三个参数:int[] arr、int a、int b

每个员工喝完咖啡有两个选择,放到咖啡机清洗队列里,或者放到水槽里自己挥发

package class13;

import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;

/**
 * 洗咖啡杯问题
 */
public class Code06_Coffee {
	
	// 方法二,洗咖啡杯的方式和原来一样,只是这个暴力版本减少了一个可变参数
	/**
	 * 例子:process(drinks, 3, 10, 0, 0)
	 * @param drinks 每一个员工喝完咖啡的时间
	 * @param a 放到洗咖啡杯机器洗,需要多少时间,固定变量
	 * @param b 咖啡杯自己挥发干净的时间,固定变量
	 * @param index 假设drinks[0...index-1]的咖啡杯都已经决定好了
	 * @param washLine 洗咖啡的机器,在washLine这个时间点才可用
	 * @return 变干净所有的咖啡杯最早的时间点
	 */
	public static int process(int[] drinks, int a, int b, int index, int washLine) {
		/**
		 * base case当我来到最后一个咖啡杯的时候
		 */
		if (index == drinks.length - 1) {
			return Math.min(
					// 洗咖啡杯机有空的时间点和我喝完的时间点的最大值,去加上a
					Math.max(washLine, drinks[index]) + a, 
					// 我喝完的时间点,去加上b
					drinks[index] + b
					);
		}
		
		// 剩不止一杯咖啡
		
		/**
		 * 情况一:index这个咖啡杯,决定用洗咖啡杯机器洗
		 */
		// wash是我当前的咖啡杯,洗完的时间
		// 洗咖啡杯机器有空的时间点和我喝完的时间点的最大值,加上a
		int wash = Math.max(washLine, drinks[index]) + a; //洗完当前一杯咖啡杯,结束的时间点
		// next1是让index+1及其后面所有的咖啡杯变干净的时间点
		int next1 = process(drinks, a, b, index + 1, wash);
		// p1是从index往后咖啡杯变干净的最早时间点
		int p1 = Math.max(wash, next1);
		
		/**
		 * 情况二:index这个咖啡杯,决定用自己挥发干净
		 */
		// dry是我喝完的时间点,加上b
		int dry = drinks[index] + b;
		// next2是让index+1及其后面所有的咖啡杯变干净的时间点
		int next2 = process(drinks, a, b, index + 1, washLine);
		// p2是从index往后咖啡杯变干净的最早时间点
		int p2 = Math.max(dry, next2);
		
		// 每个咖啡杯有两种选择,要么选择用咖啡机来洗,要么选择挥发
		return Math.min(p1, p2);
	}
	
	/**
	 * 优化成动态规划
	 * @param drinks 每一个员工喝完咖啡的时间
	 * @param a 放到洗咖啡杯机器洗,需要多少时间,固定变量
	 * @param b 咖啡杯自己挥发干净的时间,固定变量
	 * @return
	 */
	public static int dp(int[] drinks, int a, int b) {
		if (a >= b) { // 如果洗咖啡杯的时间大于挥发的时间,全部都挥发
			return drinks[drinks.length - 1] + b;
		}
		
		// 认为 a < b 的
		int N = drinks.length; // 咖啡杯的数量
		int limit = 0; // 当前咖啡机什么时候可用
		for (int i = 0; i < N; i++) {
			limit = Math.max(limit, drinks[i]) + a;
		}
		// 建一张dp表
		int[][] dp = new int[N][limit + 1];
		// N-1行,所有的值
		for (int washLine = 0; washLine <= limit; washLine++) {
			dp[N - 1][washLine] = Math.min(
					Math.max(washLine, drinks[N - 1]) + a, 
					drinks[N - 1] + b
					);
		}
		for (int index = N - 2; index >= 0; index--) {
			for (int washLine = 0; washLine <= limit; washLine++) {
				
				int p1 = Integer.MAX_VALUE;
				int wash = Math.max(washLine, drinks[index]) + a;
				if (wash <= limit) {
					p1 = Math.max(wash, dp[index+1][wash]);
				}
				int p2 = Math.max(drinks[index] + b, dp[index + 1][washLine]);
				
				dp[index][washLine] =  Math.min(p1, p2);
			}
		}
		
		return dp[0][0];
	}
	
	public static void main(String[] args) {
		// arr数组是有序增加的
		int[] arr = {1,1,5,5,7,10,12,12,12,12,12,12,15};
		int a = 3;
		int b = 10;
		
		System.out.println(process(arr,a,b,0,0));
		System.out.println(dp(arr,a,b));
		
	}
}

==============================

小结:

选择排序
插入排序
冒泡排序

单向链表和双向链表

双向链表实现栈和队列

所有递归都能改成非递归

哈希表:HashSet、HashMap
有序表:TreeMap

语言提供的堆结构,PriorityQueue优先级队列,默认是小根堆
自己实现的堆结构,数组实现的完全二叉树,heapInsert与heapify操作

比较器

桶排序:计数统计

排序算法的稳定性

二叉树:先序、中序、后序

动态规划
 

在准备考研数据结构基础知识点时,需要系统地掌握数据结构的核心概念、常见算法以及相关应用。以下是针对考研复习的基础知识整理和重点内容分析: ### 数据结构的核心概念 1. **算法的设计主要取决于数据结构**。算法的效率与数据结构的设计密切相关,选择合适的数据结构能够显著提升程序的性能。例如,在处理动态数据时,链表比顺序表更灵活;在频繁访问元素的场景下,数组比链表更高效。 2. **数据元素**是数据结构的基本单位,可以是一个数字、字符或者一组数据的集合。数据元素的组织方式直接影响数据操作的效率。 3. **时间复杂度和空间复杂度**是衡量算法性能的重要指标。时间复杂度描述算法运行时间的增长趋势,而空间复杂度描述算法所需存储空间的增长趋势。例如,冒泡排序的时间复杂度为 $ O(n^2) $,而快速排序的平均时间复杂度为 $ O(n \log n) $。 4. **逻辑结构与存储结构**:逻辑结构描述数据元素之间的逻辑关系,如线性结构、树形结构和图形结构;存储结构则是数据在计算机中的存储方式,包括顺序存储、链式存储、索引存储等。 5. **循环与递归的效率**:通常情况下,循环的效率高于递归,因为递归需要频繁地进行函数调用和栈操作,可能导致额外的内存开销。 6. **贪心算法、动态规划和分治法的区别**: - **贪心算法**每一步都选择局部最优解,期望最终达到全局最优解。 - **动态规划**通过将问题分解为子问题并存储子问题的解来避免重复计算。 - **分治法**将问题分解为多个子问题,分别求解后合并结果。 ### 线性表 7. **广义表**是一种递归的数据结构,其元素可以是原子(单个数据)或子表(嵌套的广义表)。 8. **顺序表与链表的比较**: - 顺序表支持随机访问,但插入和删除操作需要移动大量元素。 - 链表的插入和删除操作效率高,但访问元素需要从头遍历。 9. **单链表与双链表的区别**:单链表每个节点只有一个指向后继节点的指针,而双链表每个节点有两个指针,分别指向前驱和后继节点。 10. **头指针与头结点的区别**:头指针指向链表的第一个节点,而头结点是一个虚拟节点,位于链表的第一个实际节点之前。 ### 栈与队列 11. **栈与队列的区别**:栈是后进先出(LIFO)的结构,而队列是先进先出(FIFO)的结构。 12. **共享栈**是一种节省空间的栈实现方式,两个栈共享同一块存储空间。 13. **循环队列的队空与队满判断**:通过设置一个标志位或预留一个空位来区分队空和队满。 14. **栈在括号匹配中的应用**:利用栈的后进先出特性,依次将左括号压入栈,遇到右括号时判断是否匹配。 15. **栈在后缀表达式求值中的应用**:从左到右扫描后缀表达式,遇到操作数压入栈,遇到运算符弹出两个操作数计算后压入结果。 16. **栈在递归中的应用**:递归调用本质上是通过栈实现的,每次递归调用会将当前状态压入栈。 ### 树与二叉树 17. **树的性质**:树是一种非线性的数据结构,每个节点最多有一个父节点,根节点没有父节点。 18. **二叉树的存储方式**:二叉树可以通过顺序存储(数组)或链式存储(节点指针)实现。 19. **树与二叉树的转换**:树可以通过“左孩子-右兄弟”的方法转换为二叉树。 20. **树的遍历**:包括前序遍历、中序遍历和后序遍历。 ### 图 21. **图的存储方式**:邻接矩阵和邻接表是两种常见的存储方式。 22. **图的遍历**:深度优先搜索(DFS)和广度优先搜索(BFS)是图遍历的常用方法。 23. **最小生成树**:Prim算法和Kruskal算法用于构造最小生成树。 24. **最短路径**:Dijkstra算法用于求解单源最短路径,Floyd算法用于求解多源最短路径。 25. **拓扑排序**:用于判断有向无环图(DAG)是否存在环,并给出节点的线性序列。 26. **关键路径**:AOE网中的关键路径决定了工程的最短完成时间。 ### 查找与排序 27. **查找算法**:包括顺序查找、二分查找、哈希查找等。 28. **排序算法**:冒泡排序、插入排序、选择排序、快速排序、归并排序等是常见的排序算法。 29. **第K个数的排序算法**:可以使用快速选择算法,其时间复杂度为 $ O(n) $。 ### 示例代码:快速排序 ```python def quick_sort(arr): if len(arr) <= 1: return arr else: # 选择基准值 pivot = arr[len(arr) // 2] # 分别存放比基准小和大的元素 left = [x for x in arr if x < pivot] middle = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] # 递归地对左右两边进行快排,并合并结果 return quick_sort(left) + middle + quick_sort(right) ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值