目录
排序算法
冒泡排序
性能较差,不建议在大型数据集上使用。
实例:
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class TestBubbleSort {
public static void main(String[] args) {
/**
* 1.用无限流创建一个8个元素的整型流,其中8个元素均在100以内,
* 用 mapToInt() 方法把流中的元素从 Integer 类型转换成基础类型 int。
* 2.把整型流中的元素通过 toArray() 方法存放到 arr[] 中
* 3.关闭流
* */
IntStream intStream = Stream.generate(() -> (Math.random() * 100)).limit(8).mapToInt(i -> i.intValue());
int[] arr = intStream.toArray();
intStream.close();
/**
* 用冒泡排序以从大到小的顺序,重新给 arr[] 排序
* */
//打印出排序前的序列
System.out.println("排序前:"+Arrays.toString(arr));
//对需要排序的数组进行排序
for (int i = 1; i < arr.length; i++) {
//针对待排序序列中除了已经排序好的元素之外,重复排序工作
for (int j = 0; j < arr.length - i; j++) {
//当相邻两个元素需要交换时,交换相邻的两个元素
if(arr[j] < arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1]=temp;
}else {
count++;
}
}
//如果待排序序列中所有相邻两个元素都无需交换,那么,排序工作已完成,可提前跳出循环,避免浪费资源
if(count == arr.length - i){
break;
}
System.out.println("第"+ i +"次交换:"+Arrays.toString(arr));
}
//打印出排序好的序列
System.out.println("排序后:"+Arrays.toString(arr));
}
}
运行结果:
排序前:[55, 72, 43, 81, 13, 91, 48, 38]
第1次交换:[72, 55, 81, 43, 91, 48, 38, 13]
第2次交换:[72, 81, 55, 91, 48, 43, 38, 13]
第3次交换:[81, 72, 91, 55, 48, 43, 38, 13]
第4次交换:[81, 91, 72, 55, 48, 43, 38, 13]
第5次交换:[91, 81, 72, 55, 48, 43, 38, 13]
排序后:[91, 81, 72, 55, 48, 43, 38, 13]
插入排序
实例:
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class TestInsertSort {
public static void main(String[] args) {
/**
* 1.用无限流创建一个8个元素的整型流,其中8个元素均在100以内,
* 用 mapToInt() 方法把流中的元素从 Integer 类型转换成基础类型 int。
* 2.把整型流中的元素通过 toArray() 方法存放到 arr[] 中
* 3.关闭流
* */
IntStream intStream = Stream.generate(() -> (Math.random() * 100)).limit(8).mapToInt(i -> i.intValue());
int[] arr = intStream.toArray();
intStream.close();
/**
* 用插入排序以从小到大的顺序,重新给 arr[] 排序
* */
//初始化一个与待排序数组大小相同的数组,用来存放排序好的序列
int sortArray[] = new int[arr.length];
//待排序数组中选择第一个元素作为已经排序好的元素(数组的下标 0 表示第一个元素)
sortArray[0] = arr[0];
//打印出排序前的序列
System.out.println("排序前:" + Arrays.toString(arr));
//依次遍历未排序的元素,将其插入已排序序列中
for (int i = 1; i < arr.length; i++) {
//待排序元素
int temp = arr[i];
//记录待排序元素需要插入已排序数组中的位置
int index = i;
//从已排序好的数组右边依次遍历数组,直到找到待排序元素需要插入的位置
while ( index > 0 && temp < sortArray[index-1]){
sortArray[index] = sortArray[index-1];
index--;
}
//插入待排序元素
sortArray[index] = temp;
System.out.println("第"+ i +"次插入:" + Arrays.toString(sortArray));
}
//打印出排序好的序列
System.out.println("排序后:" + Arrays.toString(sortArray));
}
}
运行结果:
排序前:[7, 37, 75, 22, 67, 49, 40, 46]
第1次插入:[7, 37, 0, 0, 0, 0, 0, 0]
第2次插入:[7, 37, 75, 0, 0, 0, 0, 0]
第3次插入:[7, 22, 37, 75, 0, 0, 0, 0]
第4次插入:[7, 22, 37, 67, 75, 0, 0, 0]
第5次插入:[7, 22, 37, 49, 67, 75, 0, 0]
第6次插入:[7, 22, 37, 40, 49, 67, 75, 0]
第7次插入:[7, 22, 37, 40, 46, 49, 67, 75]
排序后:[7, 22, 37, 40, 46, 49, 67, 75]
选择排序
实例:
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class TestSelectSort {
public static void main(String[] args) {
/**
* 1.用无限流创建一个8个元素的整型流,其中8个元素均在100以内,
* 用 mapToInt() 方法把流中的元素从 Integer 类型转换成基础类型 int。
* 2.把整型流中的元素通过 toArray() 方法存放到 arr[] 中
* 3.关闭流
* */
IntStream intStream = Stream.generate(() -> (Math.random() * 100)).limit(8).mapToInt(i -> i.intValue());
int[] arr = intStream.toArray();
intStream.close();
/**
* 用选择排序以从小到大的顺序,重新给 arr[] 排序
* */
//打印出排序前的序列
System.out.println("排序前:" + Arrays.toString(arr));
//依次进行选择排序,每次找出最小的元素,放入待排序的序列中
for (int i = 0; i < arr.length; i++) {
//记录最小的元素 min 和最小元素的数组下标索引 minIndex
int min = arr[i];
int minIndex = i;
//在未排序的序列中找出最小的元素和对应数组中的位置
for (int j = i+1; j < arr.length; j++) {
if (arr[j] < min){
min = arr[j];
minIndex = j;
}
}
//交换位置
int temp = arr[i];
arr[i] = min;
arr[minIndex] = temp;
System.out.println("第"+ i +"次选择:" + Arrays.toString(arr) +
" " + "交换的数字为:" + "(" + temp + "," + min + ")");
}
//打印出排序好的序列
System.out.println("排序后:" + Arrays.toString(arr));
}
}
运行结果:
排序前:[41, 73, 12, 0, 80, 99, 44, 48]
第0次选择:[0, 73, 12, 41, 80, 99, 44, 48] 交换的数字为:(41,0)
第1次选择:[0, 12, 73, 41, 80, 99, 44, 48] 交换的数字为:(73,12)
第2次选择:[0, 12, 41, 73, 80, 99, 44, 48] 交换的数字为:(73,41)
第3次选择:[0, 12, 41, 44, 80, 99, 73, 48] 交换的数字为:(73,44)
第4次选择:[0, 12, 41, 44, 48, 99, 73, 80] 交换的数字为:(80,48)
第5次选择:[0, 12, 41, 44, 48, 73, 99, 80] 交换的数字为:(99,73)
第6次选择:[0, 12, 41, 44, 48, 73, 80, 99] 交换的数字为:(99,80)
第7次选择:[0, 12, 41, 44, 48, 73, 80, 99] 交换的数字为:(99,99)
排序后:[0, 12, 41, 44, 48, 73, 80, 99]
希尔排序(插入排序的优化)
实例:
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class TestShellSort {
public static void main(String[] args) {
/**
* 1.用无限流创建一个8个元素的整型流,其中8个元素均在100以内,
* 用 mapToInt() 方法把流中的元素从 Integer 类型转换成基础类型 int。
* 2.把整型流中的元素通过 toArray() 方法存放到 arr[] 中
* 3.关闭流
* */
IntStream intStream = Stream.generate(() -> (Math.random() * 100)).limit(8).mapToInt(i -> i.intValue());
int[] arr = intStream.toArray();
intStream.close();
/**
* 用希尔排序以从小到大的顺序,重新给 arr[] 排序
* */
//打印出排序前的序列
System.out.println("排序前:" + Arrays.toString(arr));
//初始化希尔排序的增量为数组长度
int gap = arr.length;
//不断地进行插入排序,直至增量为1
while (true){
//增量每次减半
gap /= 2;
System.out.println("增量为:" + gap);
for (int i = 0; i < gap; i++) {
//内部循环是一个插入排序
for (int j = i + gap; j < arr.length; j += gap) {
System.out.print("(K" + (j-gap) + ":" + arr[j-gap] + ",K" + j + ":" + arr[j] + ")");
int temp = arr[j];
int k = j - gap;
while (k >= 0 && arr[k] > temp){
arr[k + gap] = arr[k];
k -= gap;
}
arr[k + gap] = temp;
}
}
System.out.println("\n" + "此时的序列为:" + Arrays.toString(arr));
//增量为1之后,希尔排序结束,退出循环
if(gap == 1)break;
}
//打印出排序好的序列
System.out.println("排序后:" + Arrays.toString(arr));
}
}
运行结果:
排序前:[68, 63, 66, 50, 23, 56, 18, 49]
增量为:4
(K0:68,K4:23)(K1:63,K5:56)(K2:66,K6:18)(K3:50,K7:49)
此时的序列为:[23, 56, 18, 49, 68, 63, 66, 50]
增量为:2
(K0:23,K2:18)(K2:23,K4:68)(K4:68,K6:66)(K1:56,K3:49)(K3:56,K5:63)(K5:63,K7:50)
此时的序列为:[18, 49, 23, 50, 66, 56, 68, 63]
增量为:1
(K0:18,K1:49)(K1:49,K2:23)(K2:49,K3:50)(K3:50,K4:66)(K4:66,K5:56)(K5:66,K6:68)(K6:68,K7:63)
此时的序列为:[18, 23, 49, 50, 56, 63, 66, 68]
排序后:[18, 23, 49, 50, 56, 63, 66, 68]
快速排序
实例:
import java.util.Arrays;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class TestQuickSort {
public static void main(String[] args) {
/**
* 1.用无限流创建一个8个元素的整型流,其中8个元素均在100以内,
* 用 mapToInt() 方法把流中的元素从 Integer 类型转换成基础类型 int。
* 2.把整型流中的元素通过 toArray() 方法存放到 arr[] 中
* 3.关闭流
* */
IntStream intStream = Stream.generate(() -> (Math.random() * 100)).limit(8).mapToInt(i -> i.intValue());
int[] arr = intStream.toArray();
intStream.close();
/**
* 用快速排序以从小到大的顺序,重新给 arr[] 排序
* */
//打印出排序前的序列
System.out.println("排序前:" + Arrays.toString(arr));
//快速排序
quickSort(arr,0,arr.length-1);
//打印出排序好的序列
System.out.println("排序后:" + Arrays.toString(arr));
}
//快速排序
private static void quickSort(int[] array,int low,int high){
if(low < high){
//找到分区的位置,左边右边分别进行快速排序
int index = partition(array,low,high);
quickSort(array,0,index-1);
quickSort(array,index+1,high);
}
}
//快速排序分区操作
private static int partition(int[] array,int low,int high){
//选择基准
int pivot = array[low];
//当左指针小于右指针时,重复操作
while (low < high){
while (low < high && array[high] >= pivot){
high -= 1;
}
array[low] = array[high];
while (low < high && array[low] <= pivot){
low += 1;
}
array[high] = array[low];
}
//最后赋值基准
array[low] = pivot;
//返回基准所在位置,基准位置已经排序好
return low;
}
}
运行结果:
排序前:[46, 36, 52, 51, 51, 81, 68, 26]
排序后:[26, 36, 46, 51, 51, 52, 68, 81]
递归
递归算法
三要素:
- 递归终止条件。
- 递归终止时候的处理方法。
- 递归中重复的逻辑提取,缩小问题规模。
递归算法的伪代码:
recursion(big_problem){
if (end_condition){ //满足递归的终止条件
solve_end_condition; //处理终止条件下的逻辑
end; //递归结束
}else {
recursion(small_problem); //递归中重复的逻辑提取,缩小问题规模
}
}
递归算法之斐波那契数列
斐波那契数列,也称之为黄金分割数列。斐波那契数列指的是这样一个数列1、1、2、3、5、8、13、21、34、......,这个数列从第3项开始,每一项都等于前面两项之和。在数学上,斐波那契数列可以被递推的方法定义如下:
F(1)=1,F(2)=1,F(n)=F(n-1)+F(n-2)(n≥3,n∈N*)
而当n趋向于无穷大时,前一项与后一项的比值越来越逼近黄金分割值0.618。
斐波那契数列的递归终止条件
n=1或者n=2时,是斐波那契数列的递归终止条件,这个时候可以给出斐波那契数列的具体值。
斐波那契数列递归终止时候的处理方法
当斐波那契数列达到终止条件n=1或者n=2时,F(1)=1,F(2)=1,这就是斐波那契数列在递归终止时对应的取值。
斐波那契数列的递归重复逻辑提取
F(1)=1,F(2)=1,F(n)=F(n-1)+F(n-2)(n≥3,n∈N*)
例如,求斐波那契数列中的F(5)时为:
F(5) = F(4) + F(3) // 递归分解
= ( F(3) + F(2) ) + ( F(2) + F(1) ) // 递归求解
= [ ( F(2) + F(1) ) + 1 ] + ( 1 + 1 ) // 递归求解,遇到终止条件就求解
= [ (1 + 1) + 1]+(1 + 1) // 归并
= 3 + 2 // 归并
= 5 // 归并
实例:
public class TestFibonacci {
public static void main(String[] args) {
for (int i = 1; i < 8; i++) {
System.out.println("F(" + i + ") = " + fibonacci(i));
}
}
//斐波那契数列的计算
private static int fibonacci(int n){
//如果是终止条件,按照要求返回终止条件对应结果
if(n == 1 || n == 2){
return 1;
}else {
//非终止条件,按照要求把大的问题拆分成小问题,调用自身函数递归处理
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
}
运行结果:
F(1) = 1
F(2) = 1
F(3) = 2
F(4) = 3
F(5) = 5
F(6) = 8
F(7) = 13
分治
分治算法
分治法就是将一个复杂的大问题分解成两个或者更多相同或者相似的子问题,再把子问题继续拆分成更小的子问题,直到子问题可以直接求解,然后原问题的解就是子问题的解的合并。
分治法所能够解决的问题一般都会具有如下几个特征
- 待求解问题可以拆分成相同形式的规模较小的问题。
- 待求解问题在规模缩小到一定程度之后就可以很容易求解。
- 利用待求解问题拆分出的子问题的解可以合并为待求解问题的解。
- 待求解问题拆分出来的子问题是相互独立的,子问题之间不会包含相同的子问题。
分治法的求解步骤
步骤1:将待求解问题拆分成若干个规模较小、相互独立的子问题,子问题的形式与待求解问题相同
步骤2:若干个子问题如果很容易求解则直接求解,如果不容易求解则递归的解各个子问题
步骤3:将各个子问题分别求解的结果合并成待求解问题的解
分治算法基于求解步骤的伪代码:
divideAndConquer(big_problem){
if (canSolve(big_problem)){ //问题可以直接求解则直接求解返回
solve(big_problem); //求解
return;
}else {
small_problem_A = divide(big_problem); //不能直接求解的问题拆分
small_problem_B = divide(big_problem); //不能直接求解的问题拆分
divideAndConquer(small_problem_A); //递归求解子问题
divideAndConquer(small_problem_B); //递归求解子问题
return merge(); //合并子问题的解
}
}
实例1:二分搜索
二分搜索是在一个有序的数组中,通过均匀二分,每次折半查找,就是应用到分治算法中将大问题缩减到小问题,这个小问题的最后结果就是刚好找到需要查找搜索的元素,这样小问题得出解,这个解也是最开始的待搜索的元素。
实例2:全排列问题
比如有3个小朋友排成一列,问你一共有多少种排列的情况,这个问题类似于数学中的全排列问题。
先依次从三个小朋友中选择一位排在队列最前面,剩下的两个小朋友可以进行全排列,也可以继续拆分,二者选择其一进行即可,这个时候其实很清楚,他们只有两种排列情况了,然后跟前面小朋友排列组合在一起。
比如用A,B,C代表三个小朋友,排列情况如下:
//初始需要排列的元素
[A,B,C]//依次从三个小朋友中选择一位排在队列最前面,剩下的两个小朋友全排列
A[B,C]; B[A,C]; C[A,B]//剩下的两个小朋友排列情况只有两种,与之前的合并在一起
ABC,ACB,BAC,BCA,CAB,CBA
分治算法之最大子数组问题
最大子数组问题描述如下:
假如我们有一个数组,数组中的元素有正数和负数,如何在数组中找到一段连续的子数组,使得子数组各个元素之和最大。
最大子数组问题在生活中的实例:比如我们观察某一个股票在一段时间内的走势,请问如何找出在哪一天买入,哪一天卖出可以赚到最大差价(这里假设你已经知道股票的走势)?为了实现最大化的股票收益,我们需要考虑的是买进和卖出时候的价格变化幅度,因此从该股票的每日变化幅度来考虑这个问题更加合适。所以,我们可以将这个问题稍作变形:将股票价格走势对应为每日股票价格涨跌,涨记为正值,跌记为负值,然后一段时间就对应一个正负数数组,并试图找到该数组的最大子数组,就可以获得最大收益。
假设待求解的数组为 A , low 和 high 是这个数组的最小和最大下标,low=0,high=A.length-1,然后我们要找到该数组的其中某一个最大子数组。
Tips:同一个数组的最大子数组可能有多个
求解思路:
步骤1:找到数组 A 的中间元素,其下标记为 mid ,根据分治策略,将数组A[low,high]根据中间元素划分为A[low,mid],A[mid+1,high]两个部分
步骤2:假设数组A的最大子数组A[i,j],那么A[i,j]只有以下3种可能:
a. 最大子数组A[i,j]完全位于A[low,mid]中,此时有 low <= i <= j <= mid
b. 最大子数组A[i,j]完全位于A[mid+1,high]中,此时有 mid+1 <= i <= j <= high
c. 最大子数组A[i,j]跨越了中间元素,则 low <= i <= mid <= j <= high
步骤3:对步骤2三种情况的求解结果进行比较,其中最大子数组的结果为最大值的情况就是我们的所求结果
求解思路:
如图,步骤2中的情况 a 和情况 b 可以通过递归求解得出结果。而情况 c 的并不是原问题的一个规模更小的实例,因为情况 c 中加入了一个限制(求出的子数组必须跨越下标 mid 的中间节点)。如上图的右边图形所示,情况 c 的求解结果都会有两个子数组 A[i,mid] 和 A[mid+1,j] 组成,其中 low <= i <= mid <= j <= high。因此,我们只需要找出形如 A[i,mid] 和 A[mid+1,j] 的最大子数组,并将其合并即可。
步骤2 中的情况 c 具体实现的伪代码:
FindMaxCrossSubarray(A, low, mid ,high):
leftSum = minInteger; //设置左边的最大连续和初始值为最小整数值
sum =0;
maxLeft = mid; //记录左边最大子数组的下标位置,初始化为mid
for (i=mid; i>=low; i--){
sum = sum + A[i];
if (sum > leftSum){
leftSum = sum;
maxtLeft = i;
}
}
rightSum = minInteger; //设置右边的最大连续和初始值为最小整数值
sum = 0;
maxtRight = mid + 1; //记录右边最大子数组的下标位置,初始化为mid+1
for (j=mid+1; j<=low; j++){
sum = sum + A[j];
if (sum > rightSum){
rightSum = sum;
maxtRight = j;//记录左边最大子数组的下标位置
}
}
//返回结果是一个三元组数据,分别是最大子数组的开始下标,结束下标,求和的值
return (maxLeft,maxRight,leftSum+rightSum);
数组 A 求解最大子数组的整体过程的伪代码:
FindMaxSubarray(A,low,high):
if (high == low){
return new Result(low,high,A[low]); //基础情况,只有一个元素时候的处理情况
}else {
//对应2.1节中步骤1,找到中间元素
int mid = (low + high)/2;
//对应2.1节中步骤2,分别对应a,b,c三种情况求解最大子数组结果
(leftLow,leftHigh,leftSum) = FindMaxSubarray(A,low,mid);
(rightLow,rightHigh,rightSum) = FindMaxSubarray(A,mid+1,high);
(crossLow,crossHigh,crossSum) = FindMaxCrossSubarray(A,low,mid,high);
//对应2.1节中步骤3,比较得出最后结果
if(leftSum >= righSum && leftSum >= crossSum){
return (leftLow,leftHigh,leftSum);
}else if (rightSum >= leftSum && rightSum >= crossSum){
return (rightLow,rightHigh,rightSum);
}else {
return (crossLow,crossHigh,crossSum);
}
}
JAVA 代码实现
实例:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
public class MaxSubarray {
//用来存储最大子数组的返回结果
private static class Result{
int low;
int high;
int sum;
public Result(int low, int high, int sum) {
this.low = low;
this.high = high;
this.sum = sum;
}
@Override
public String toString() {
return "Result{" +
"low=" + low +
", high=" + high +
", sum=" + sum +
'}';
}
}
private static Result FindMaxCrossSubarray(int[] A,int low,int mid,int high){
System.out.println(Arrays.toString(A));
//寻找左边的连续最大值及记录位置
int leftSum = Integer.MIN_VALUE;
int sum = 0;
int maxLeft = mid;
for (int i = mid; i >= low; i--) {
sum = sum + A[i];
if(sum > leftSum){
leftSum = sum;
maxLeft = i;
}
System.out.println("(左边此时的最大值:" + leftSum + ",左边每偏移一个位置的值:" + sum +
",左边此时最大值的下标:" + maxLeft + ")");
}
//寻找右边的连续最大值及记录位置
int rightSum = Integer.MIN_VALUE;
sum = 0;
int maxRight = mid + 1;
for (int j = mid + 1; j <= high; j++) {
sum = sum + A[j];
if(sum > rightSum){
rightSum = sum;
maxRight = j;
}
System.out.println("(右边此时的最大值:" + leftSum + ",右边每偏移一个位置的值:" + sum +
",右边此时最大值的下标:" + maxRight + ")");
}
//返回跨越中间值的最大子数组结果
return new Result(maxLeft,maxRight,leftSum + rightSum);
}
public static Result FindMaxSubarray(int[] A,int low,int high){
//数组只有一个元素时的处理情况
if(high == low){
return new Result(low,high,A[low]);
}else {
//找到中间元素
int mid = (low + high) / 2;
//求解三种情况最大子数组结果
/**
* 当 high != low的情况下,求 leftResult 的递归过程中,也会同时求解 rightResult 和 crossResult。
* 同理,当 high != low的情况下,求 rightResult 的递归过程中,也会同时求解 leftResult 和 crossResult。
* */
System.out.println("leftFindMaxSubarray(A,low:" + low + ",high:" + mid +")");
Result leftResult = FindMaxSubarray(A,low,mid);
System.out.println("rightFindMaxSubarray(A,low:" + (mid + 1) + ",high:" + high +")");
Result rightResult = FindMaxSubarray(A,mid + 1,high);
System.out.println("FindMaxCrossSubarray(A,low:" + low + "," + "mid:" + mid + "," +
"high:" + high + ")");
Result crossResult = FindMaxCrossSubarray(A,low,mid,high);
//比较
if (leftResult.sum >= rightResult.sum && leftResult.sum >= crossResult.sum){
return leftResult;
}else if (rightResult.sum >= leftResult.sum && rightResult.sum >= crossResult.sum){
return rightResult;
}else {
return crossResult;
}
}
}
public static void main(String[] args) {
List<Integer> lists = new ArrayList<>();
Random random = new Random();
int randomNumber;
for (int i = 0; i < 13; i++) {
//生成一个-20到20的随机数,包括-20和20
randomNumber = random.nextInt(41) - 20;
lists.add(randomNumber);
}
int[] A = lists.stream().mapToInt(i -> i.intValue()).toArray();
System.out.println(Arrays.toString(A));
Result result = FindMaxSubarray(A,0,A.length-1);
System.out.println("最大子数组为:A[" + result.low + "," + result.high + "],数组总和为:" + result.sum);
}
}
运行结果:
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
leftFindMaxSubarray(A,low:0,high:6)
leftFindMaxSubarray(A,low:0,high:3)
leftFindMaxSubarray(A,low:0,high:1)
leftFindMaxSubarray(A,low:0,high:0)
rightFindMaxSubarray(A,low:1,high:1)
FindMaxCrossSubarray(A,low:0,mid:0,high:1)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:-16,左边每偏移一个位置的值:-16,左边此时最大值的下标:0)
(右边此时的最大值:-16,右边每偏移一个位置的值:18,右边此时最大值的下标:1)
rightFindMaxSubarray(A,low:2,high:3)
leftFindMaxSubarray(A,low:2,high:2)
rightFindMaxSubarray(A,low:3,high:3)
FindMaxCrossSubarray(A,low:2,mid:2,high:3)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:5,左边每偏移一个位置的值:5,左边此时最大值的下标:2)
(右边此时的最大值:5,右边每偏移一个位置的值:-13,右边此时最大值的下标:3)
FindMaxCrossSubarray(A,low:0,mid:1,high:3)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:18,左边每偏移一个位置的值:18,左边此时最大值的下标:1)
(左边此时的最大值:18,左边每偏移一个位置的值:2,左边此时最大值的下标:1)
(右边此时的最大值:18,右边每偏移一个位置的值:5,右边此时最大值的下标:2)
(右边此时的最大值:18,右边每偏移一个位置的值:-8,右边此时最大值的下标:2)
rightFindMaxSubarray(A,low:4,high:6)
leftFindMaxSubarray(A,low:4,high:5)
leftFindMaxSubarray(A,low:4,high:4)
rightFindMaxSubarray(A,low:5,high:5)
FindMaxCrossSubarray(A,low:4,mid:4,high:5)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:-4,左边每偏移一个位置的值:-4,左边此时最大值的下标:4)
(右边此时的最大值:-4,右边每偏移一个位置的值:-20,右边此时最大值的下标:5)
rightFindMaxSubarray(A,low:6,high:6)
FindMaxCrossSubarray(A,low:4,mid:5,high:6)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:-20,左边每偏移一个位置的值:-20,左边此时最大值的下标:5)
(左边此时的最大值:-20,左边每偏移一个位置的值:-24,左边此时最大值的下标:5)
(右边此时的最大值:-20,右边每偏移一个位置的值:-18,右边此时最大值的下标:6)
FindMaxCrossSubarray(A,low:0,mid:3,high:6)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:-13,左边每偏移一个位置的值:-13,左边此时最大值的下标:3)
(左边此时的最大值:-8,左边每偏移一个位置的值:-8,左边此时最大值的下标:2)
(左边此时的最大值:10,左边每偏移一个位置的值:10,左边此时最大值的下标:1)
(左边此时的最大值:10,左边每偏移一个位置的值:-6,左边此时最大值的下标:1)
(右边此时的最大值:10,右边每偏移一个位置的值:-4,右边此时最大值的下标:4)
(右边此时的最大值:10,右边每偏移一个位置的值:-24,右边此时最大值的下标:4)
(右边此时的最大值:10,右边每偏移一个位置的值:-42,右边此时最大值的下标:4)
rightFindMaxSubarray(A,low:7,high:12)
leftFindMaxSubarray(A,low:7,high:9)
leftFindMaxSubarray(A,low:7,high:8)
leftFindMaxSubarray(A,low:7,high:7)
rightFindMaxSubarray(A,low:8,high:8)
FindMaxCrossSubarray(A,low:7,mid:7,high:8)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:-1,左边每偏移一个位置的值:-1,左边此时最大值的下标:7)
(右边此时的最大值:-1,右边每偏移一个位置的值:10,右边此时最大值的下标:8)
rightFindMaxSubarray(A,low:9,high:9)
FindMaxCrossSubarray(A,low:7,mid:8,high:9)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:10,左边每偏移一个位置的值:10,左边此时最大值的下标:8)
(左边此时的最大值:10,左边每偏移一个位置的值:9,左边此时最大值的下标:8)
(右边此时的最大值:10,右边每偏移一个位置的值:-6,右边此时最大值的下标:9)
rightFindMaxSubarray(A,low:10,high:12)
leftFindMaxSubarray(A,low:10,high:11)
leftFindMaxSubarray(A,low:10,high:10)
rightFindMaxSubarray(A,low:11,high:11)
FindMaxCrossSubarray(A,low:10,mid:10,high:11)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:-19,左边每偏移一个位置的值:-19,左边此时最大值的下标:10)
(右边此时的最大值:-19,右边每偏移一个位置的值:-19,右边此时最大值的下标:11)
rightFindMaxSubarray(A,low:12,high:12)
FindMaxCrossSubarray(A,low:10,mid:11,high:12)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:-19,左边每偏移一个位置的值:-19,左边此时最大值的下标:11)
(左边此时的最大值:-19,左边每偏移一个位置的值:-38,左边此时最大值的下标:11)
(右边此时的最大值:-19,右边每偏移一个位置的值:-14,右边此时最大值的下标:12)
FindMaxCrossSubarray(A,low:7,mid:9,high:12)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:-6,左边每偏移一个位置的值:-6,左边此时最大值的下标:9)
(左边此时的最大值:4,左边每偏移一个位置的值:4,左边此时最大值的下标:8)
(左边此时的最大值:4,左边每偏移一个位置的值:3,左边此时最大值的下标:8)
(右边此时的最大值:4,右边每偏移一个位置的值:-19,右边此时最大值的下标:10)
(右边此时的最大值:4,右边每偏移一个位置的值:-38,右边此时最大值的下标:10)
(右边此时的最大值:4,右边每偏移一个位置的值:-52,右边此时最大值的下标:10)
FindMaxCrossSubarray(A,low:0,mid:6,high:12)
[-16, 18, 5, -13, -4, -20, -18, -1, 10, -6, -19, -19, -14]
(左边此时的最大值:-18,左边每偏移一个位置的值:-18,左边此时最大值的下标:6)
(左边此时的最大值:-18,左边每偏移一个位置的值:-38,左边此时最大值的下标:6)
(左边此时的最大值:-18,左边每偏移一个位置的值:-42,左边此时最大值的下标:6)
(左边此时的最大值:-18,左边每偏移一个位置的值:-55,左边此时最大值的下标:6)
(左边此时的最大值:-18,左边每偏移一个位置的值:-50,左边此时最大值的下标:6)
(左边此时的最大值:-18,左边每偏移一个位置的值:-32,左边此时最大值的下标:6)
(左边此时的最大值:-18,左边每偏移一个位置的值:-48,左边此时最大值的下标:6)
(右边此时的最大值:-18,右边每偏移一个位置的值:-1,右边此时最大值的下标:7)
(右边此时的最大值:-18,右边每偏移一个位置的值:9,右边此时最大值的下标:8)
(右边此时的最大值:-18,右边每偏移一个位置的值:3,右边此时最大值的下标:8)
(右边此时的最大值:-18,右边每偏移一个位置的值:-16,右边此时最大值的下标:8)
(右边此时的最大值:-18,右边每偏移一个位置的值:-35,右边此时最大值的下标:8)
(右边此时的最大值:-18,右边每偏移一个位置的值:-49,右边此时最大值的下标:8)
最大子数组为:A[1,2],数组总和为:23
动态规划
动态规划算法
动态规划算法与分治算法相似,都是通过组合子问题的解来求解原问题的解,但是两者之间也有很大区别:分治法将问题划分为互不相交的子问题,递归的求解子问题,再将他们的解组合起来求解原问题的解。与之相反,动态规划应用于子问题相互重叠的情况,在这种情况下,分治法还是会做很多重复的不必要的工作,他会反复求解那些公共的子问题,而动态规划算法则对相同的每个子问题只会求解一次,将其结果保存起来。
Tips:这里说到的动态规划应用于子问题相互重叠的情况,是指原问题不同的子问题之间具有相同的更小的子子问题,他们的求解过程和结果完全一样
动态规划算法求解的问题都会具备以下两点性质
1.最优子结构:如果一个问题的最优解包含其子问题的最优解,则此问题具备最优子结构的性质。因此,判断某个问题是否适合用动态规划算法,需要判断该问题是否具有最优子结构。
Tips: 最优子结构的定义主要是在于当前问题的最优解可以从子问题的最优解得出,当子问题满足最优解之后,才可以通过子问题的最优解获得原问题的最优解。
2.重叠子问题:问题的子问题空间必须最够“小”,也就是说原问题递归求解时会重复相同的子问题,而不是一直生成新的子问题。如果原问题的递归算法反复求解相同的子问题,我们就称该最优化问题具有重叠子问题。
动态规划算的求解步骤
步骤1:刻画一个最优解的结构特征
一般都是用一些数学方法去描述求解问题,用数学公式表明最优解的结构特征。
步骤2:递归的定义最优解的值
递归的求解相同的子问题,通常也是先用数学公式去递归定义。
步骤3:计算最优解的值
当清楚刻画出一个最优解的结构特征及可以递归的定义出最优解的值之和,我们就可以采用自底向上的方法逐步去计算每一个最优解的值,大问题的最优解的值依赖于小问题的最优解的值
步骤4:利用计算出的信息构造一个最优解
步骤1,2,3是动态规划算法求解问题的基础,如果我们仅仅需要一个最优解的值,而不是需要了解最优解本身,我们可以不执行步骤4。如果我们需要了解最优解的具体情况,我们就需要再执行步骤3的时候维护一些额外的信息,以便用来构造出一个最优解。
动态规划示例问题——爬楼梯
假设你正在爬楼梯,一共需要经过 n 阶楼梯你才可以到达楼顶。每次你可以爬楼梯的 1 或 2 个台阶。请问一共有多少种不同的方法可以爬到楼顶?
步骤1:刻画爬楼梯问题的一个最优解的结构特征
情况1:输入n=1,输出为1
解释1:楼梯只有1阶,爬1步是最优解,记为1
情况2:输入n=2,输出为2
解释1:楼梯有2阶,有两种情况可以爬上楼顶,分别为连续两次爬1步和一次爬两部,记为1+1,2
情况3:输入n=3,输出为3
解释1:楼梯有3阶,有三种情况可以爬上楼顶,如情况1和2描述一样,记为1+1+1,2+1,1+2
爬楼梯问题主要在于我们可以一次爬 2 步或者 1 步,所以到达最后一阶楼梯 n 时,我们可以从第 n-2 阶楼梯爬 2 步或者第 n-1 阶楼梯爬 1 步完成。
当我们需要知道最多有多少中方法可以爬上 n 阶的楼梯时,我们需要分别知道爬上第 n-2 阶楼梯最多有多少中方法,爬上第 n-1 阶楼梯最多有多少中方法,然后爬上第 n 阶楼梯的最多方法数量等于爬上第 n-1 阶楼梯最多的方法数量加上爬上第 n-2 阶楼梯最多的方法数量。
Tips: 爬楼梯问题满足动态规划算法所需的两种性质,其中的
最优子结构就是:爬上 n 阶楼梯的最多方法数包含爬上第 n-1 阶楼梯和第 n-2 阶楼梯的最多方法数。
重叠子问题:需要反复的计算爬各阶楼梯的最多方法数。
步骤2:递归的定义爬 n 阶楼梯最多的方法数
- 上 1 阶台阶:有 1 种方法
- 上 2 阶台阶:有 1+1 和 2 两种方法
- 上 3 阶台阶:到达第 3 阶的方法总数是到达第 1 阶和第 2 阶方法的总和
- 上 n 阶台阶:到达第 n 阶的方法总数是到达第 (n-1) 阶和第 (n-2) 阶的方法数之和
综上所述,爬 n 阶楼梯的状态转移方程可以定义为:goStep(n) = goStep(n-1)+goStep(n-2) 。
步骤3:计算爬 n 阶楼梯最多方法数的值
楼梯阶数 n | 爬 n 阶楼梯最多的方法数 |
---|---|
1 | 1 |
2 | 2 |
3 | goStep(1)+goStep(2)=1+2=3 |
4 | goStep(2)+goStep(3)=2+3=5 |
5 | goStep(3)+goStep(4)=3+5=8 |
6 | goStep(4)+goStep(5)=5+8=13 |
7 | goStep(5)+goStep(6)=8+13=21 |
8 | goStep(6)+goStep(7)=13+21=36 |
9 | goStep(7)+goStep(8)=21+36=57 |
步骤4:利用计算出的信息构造爬 n 阶楼梯每次走几步的方法
我们并不需要统计每次的具体爬楼梯方法,如果需要统计每次具体走法时,需要再计算的时候记录之前的每一步走法,把信息全部记录保留下来即可。
动态规划算法很多时候都是应用于求解一些最优化问题(最大,最小,最多,最少)
动态规划之钢条切割问题
某钢材公司购买长钢条,将其切割为短钢条出售,其中切割过程本身不考虑成本,问最赚钱的钢材切割方案。假设钢材公司出售一段长度为 i 米的钢条的价格为 p(i),对应价目表如下:
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
p(i) | 1 | 5 | 8 | 9 | 10 | 17 | 17 | 20 | 24 | 30 |
钢材切割问题的定义:给定一段长度为 n 米的钢条和对应的一个价格表 (p(i),i=1,2,3,...n),求一个钢条切割方案,使得最终的销售收益 r(n) 最大。(切割的钢条必须为整米长度)
问题分析:
首先,一段长度为 n 米的钢条一共有 2n -1 种切割方案,因为在钢条的第 1,2,3 ..., n-1 米的位置,均可以选择切割或者不切割。对于一段长度为 n 米钢条,假设我们将他切割为 k 段,每一长度段记为 i1,i2 ...,ik 米,我们可以将切割方案记为 n = i1 + i2 + ... + ik,对应的收益 r(n) = p(i1) + p(i2) + ... + p(ik) 。接着,按动态规划算法求解步骤求解问题。
步骤1:刻画一个钢条切割最优解的结构特征
因为在钢条的第 1,2,3 ..., n-1 米的位置,均可以选择切割或者不切割。现在我们考虑将一段长度为 n 米钢条切割的最大收益 r(n) 用小一点的钢材收益表示,假设这个时候我们可以选择切割一次或者不切割,那对应着的 n 米的钢材会有 n 种处理方案,分别为:{p(n) , r(1) + r(n-1), r(2) + r(n-2) , ... , r(n-2) + r(2) , r(n-1) + r(1)},这里的 p(n) 表示没有切割,这样我们就可以将计算一段长度为 n 米的钢材最优化切割方案转换为小一点长度的钢材的最优化切割方案。
为了求解规模为 n 的问题,我们先求解形式完全一样,单规模更小的问题。当完成首次切割之后,我们将两段钢材看成两个独立的钢条切割问题。我们通过组合两个相关子问题的最优解,并在所有可能得两段切割方案中选择组合收益最大者,构成原问题的最优解。钢条切割问题满足最优子结构性质:问题的最优解由相关子问题的最优解组合而成,而这些子问题可以独立求解。
步骤2:递归的定义钢条切割的最优解
列举前面几种简单的钢条切割的最优解:
r(1)= 1,钢条长度为1米的钢条最优切割方法就是自身,因为已经无法切割了
r(2)= 5,钢条长度为2米的钢条最优切割方案有两种,1+1或者2,对应收益为 2 或 5,所以r(2)= 5
r(3)= 8,钢条长度为2米的钢条最优切割方案有三种,3,1+2,2+1,对应收益为 8、6、6 ,所以r(3)= 8
对应步骤1中的钢条切割问题的最优解的结构特征,递归的定义钢条切割问题的最优解:
r(n) = max {p(n) , r(1) + r(n-1), r(2) + r(n-2) , ... , r(n-2) + r(2) , r(n-1) + r(1)}
钢条切割问题还可以以另外一种相似的但是更为简单的方法去求解:将钢条左边切割下长度为 i 米的一段,只对右边剩下的长度为 n-i 的一段进行继续切割(递归求解),对左边的一段则不再进行切割。
此时,问题可以分解为:将长度为 n 的钢条分解为左边开始一段以及剩余部分继续分解的结果。基于上面公式的简化版本为:
r(n) = max { p(i) + r(n-i) },1 <= i <= n
步骤3:计算钢条切割最优解的值
对于长度为 n 的钢条切割的最优解由其子问题决定,我们可以先求解长度较小的钢条切割的最优解,然后用较小长度的最优解去逐步求解长度较大的钢条切割的最优解。伪代码如下:
CutSteelRod(p,n):{
r[0...n] be a new array[]
r[0]=0
for (int i=1; i<=n; i++){
q = Integer.MIN_VALUE
for (int j=1;j<=i;j++){
q = max(q,p[j]+r[i-j])
}
r[i]=q
}
return r[n]
}
代码解析:第 2 行定义了一个新数组 r[0...n] 用来存储子问题的最优解,第 3 行将 r[0] 初始化为 0,因为长度为 0 的钢条没有收益。第 4 行到第 10 行是两个 for 循环,外层 for 循环分别求解长度为 i 的钢条切割的最优解,内层 for 循环是每一次求解最优解的具体过程。
步骤4:利用计算出的信息构造一个钢条切割问题的最优解
前面步骤3并不会返回解本身(对应的一个长度列表,给出每段钢条的切割长度),如果我们需要得出具体的解,就需要对步骤3中的算法进行重构,伪代码如下:
ExtendCutSteelRod(p,n){
r[0...n],s[0...n] be new arrays[]
r[0]=0
for (int i=1; i<=n; i++){
q = Integer.MIN_VALUE
for (int j=1;j<=i;j++){
if(q < p[j]+r[i-j]){
q = p[j]+r[i-j]
s[i] = j
}
}
r[i]=q
}
return r and s
}
代码解析:算法第 2 行多创建了数组 s,并在求解规模为 i 的子问题时将第一段钢条的最优切割长度 j 保存在 s[i] 中。
JAVA 代码实现
实例:
import java.util.*;
public class SteelBarCutProblem {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
//初始化对应长度的钢材的价格表
int[] p = {0,1,5,8,9,10,17,17,20,24,30};
//对应长度钢条切割之后的最大收益数组
int[] r = new int[p.length];
//对应长度钢条满足最大化收益时第一次切割的长度
int[] s = new int[p.length];
System.out.println("请输入1到" + (p.length-1) + "之间任意一个自然数:");
int n = scanner.nextInt();
if(n >=1 && n <= p.length-1){
r[0] = 0;
for (int i = 1; i <= n; i++) {
int q = Integer.MIN_VALUE;
for (int j = 1; j <= i; j++) {
if(q < (p[j] + r[i-j])){
q = p[j] + r[i-j];
s[i] = j;
}
}
r[i] = q;
}
System.out.println("长度为" + n + "米长的钢材最大切割收益为:" + r[n]);
System.out.println("对应的具体每一段的切割长度如下:");
while (n>0){
System.out.println(s[n]);
n = n - s[n];
}
}else {
System.out.println("数据输入错误");
}
}
}
运行结果:
请输入1到10之间任意一个自然数:
9
长度为9米长的钢材最大切割收益为:25
对应的具体每一段的切割长度如下:
3
6
贪心算法
贪心算法使用条件具备以下两个性质:
1.贪心选择:当某一个问题的整体最优解可通过一系列局部的最优解的选择达到,并且每次做出的选择可以依赖以前做出的选择,但不需要依赖后面需要做出的选择。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
2.最优子结构: 如果一个问题的最优解包含其子问题的最优解,则此问题具备最优子结构的性质。问题的最优子结构性质是该问题是否可以用贪心算法求解的关键所在。
实际应用场景:
场景1:活动选择问题
假如小明是一个勤工俭学的在校生,每天可以兼职10小时,然后现在学校有多个不同的兼职岗位,每个岗位有个开始时间和结束时间,小明在同一时间只能做一份兼职,请问小明每天最多可以做多少份兼职?
面对这样的问题,我们第一选择肯定是先把结束时间早的兼职活动做了,然后再去做其他的兼职。这里面其实就是贪心思想的应用,因为我们每次都是先做完结束时间早的兼职,然后我们会留出更多的时间可以做其他兼职。
这个问题里面的贪心选择就是每次选择最先结束的兼职活动,并且可以证明每次选择的最先结束的兼职活动是整体最优的,因为如果不是选择最先结束的兼职活动,剩下的可兼职的时间就少了,可以完成的兼职也就少了。同样,兼职选择活动具有最优子结构的性质,子问题可以选择的最多兼职在整体问题中同样适用。
场景2:钱币找零问题
有1元、2元、5元、10元的纸币分别有若干张,要用这些纸币凑出 m 元,至少要用多少张纸币?
比如在商场中用现金付款时,我们付出了100元,当收银员找零的时候,收银员总是会先找零面额较大的货币,然后找零面额较小的货币,这个其实也是一个贪心选择的过程,因为这样可以保证找零的货币数量最少。
贪心算法之活动选择问题
假设我们有一个 n 个活动的集合 S={a1,a2,a3, ...an},这些活动需要使用同一个资源,但是这个资源在某一个时刻只能被一个活动占用,并且每一个活动 ai 都有一个开始时间 si 和结束时间 fi,其中 si < fi ,并且开始时间和结束时间都在可以选择的活动时间范围内。这里,如果某个活动 ai 被选中,则我们可以说活动 ai 发生在半开时间区间 [si,fi) 内,如果两个活动 ai 和 aj 满足 [si,fi) 和 [sj,fi) 不重叠,则称他们是兼容的。也就是说,若 si >= fj 或者 sj >= fi,则称 ai 和 aj 是相互兼容的,在活动选择问题中,我们希望选出一个最大兼容活动集。
例如:学校里面有一个大阶梯教室,可以用来上公开课。这个阶梯教室就相当于是一个资源,不同的公开课,比如数学课、语文课、英语课等等,这就是一个个活动,并且每节课都有一个开始时间和结束时间,活动选择问题就要求我们选择出所有的可以在阶梯教室中安排的课程,保证选出的课程集合是一个最大的兼容活动集。
活动集合如下:
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
si | 1 | 3 | 0 | 5 | 3 | 5 | 6 | 8 | 8 | 2 | 12 |
fi | 4 | 5 | 6 | 7 | 9 | 9 | 10 | 11 | 12 | 14 | 16 |
伪代码:
GreedyActivitySelect(a,s,f):
//定义活动总数
n = s.length
//按照贪心策略,首先选中第一个结束的活动
A = {a[i]}
//记录当前选中的活动
k = 1
//for循环遍历,按照贪心策略选择活动
for i=2 to n{
if s[i] >= f[k]{
A = A.add(a[i])
k = i
}
}
JAVA 代码实现
实例:
import java.util.ArrayList;
import java.util.List;
public class ActivitySelect {
public static void main(String args[]){
//活动集合a
int a[] = {1,2,3,4,5,6,7,8,9,10,11};
//活动开始时间集合s
int s[] ={1,3,0,5,3,5,6,8,8,2,12};
//活动结束时间集合f
int f[] ={4,5,6,7,9,9,10,11,12,14,16};
//活动选择存放集合A
List<Integer> A = new ArrayList<>();
int n = s.length;
A.add(a[0]);
int k =0;
//遍历选择活动
for (int i=1; i<n; i++){
if(s[i] >= f[k]){
A.add(a[i]);
k = i;
}
}
System.out.println("活动选择问题的选择活动结果为:");
System.out.println(A);
}
}
运行结果:
活动选择问题的选择活动结果为:
[1, 4, 8, 11]
贪心算法之背包问题
假设我们一共有 n 种物品,每种物品 i 的价值为 vi,重量为 wi,我们有一个背包,背包的容量为 c(最多可以放的物品重量不能超过 c),我们需要选择物品放入背包中,使得背包中选择的物品中总价值最大,在这里每个物品可以只选择部分。
容量为30,各类物品及对应的重量和价值如下:
物品 n(i) | 1 | 2 | 3 | 4 | 5 |
重量 w(i) | 10 | 5 | 15 | 10 | 20 |
价值 v(i) | 20 | 30 | 15 | 25 | 10 |
问题分析:
对于背包问题,很显然它是最优子结构性质的,因为一个容量 c 的背包问题必然包含容量小于 c 的背包问题的最优解的。
如果要使得最终的价值最大,那么必定需要使得选择的单位重量的物品的价值最大。所以背包问题的贪心选择策略是:优先选择单位重量价值最大的物品,当这个物品选择完之后,继续选择其他价值最大的物品。
求解详解:
第一步,计算每个物品的单位价值,如下:
unitValue(1) = 20/10 = 2
unitValue(2) = 30/5 = 6
unitValue(3) = 15/15 = 1
unitValue(4) = 25/10 = 2.5
unitValue(5) = 10/20 = 0.5unitValue(2) > unitValue(4) > unitValue(1) > unitValue(3) > unitValue(5)
当背包容量为30,我们发现可以按物品的单价进行依次放入,直至背包容量放满或者物品放完为止,过程如下:
物品类型 | 放入重量 | 背包使用容量 | 背包剩余容量 |
---|---|---|---|
2 | 5 | 5 | 25 |
4 | 10 | 15 | 15 |
1 | 10 | 25 | 5 |
3 | 5 | 30 | 0 |
JAVA 代码实现
实例:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Knapsack {
/**
* 物品内部类
*/
private static class Item implements Comparable<Item>{
int type;
double weight;
double value;
double unitValue;
public Item(int type, double weight){
this.type = type;
this.weight = weight;
}
public Item(int type, double weight,double value){
this.type = type;
this.weight = weight;
this.value = value;
this.unitValue = value/weight;
}
@Override
public int compareTo(Item o) {
return Double.valueOf(o.unitValue).compareTo(this.unitValue);
}
}
public static void main(String[] args){
//背包容量
double capacity = 30;
//物品类型初始化数组
int[] itemType = {1,2,3,4,5};
//物品重量初始化数组
double[] itemWeight = {10,5,15,10,30};
//物品价值初始化数组
double[] itemValue = {20,30,15,25,10};
//初始化物品
List<Item> itemList = new ArrayList<>();
for(int i=0;i<itemType.length;i++){
Item item = new Item(itemType[i],itemWeight[i],itemValue[i]);
itemList.add(item);
}
//物品按照单价降序排序
Collections.sort(itemList);
//背包选择
List<Item> selectItemList = new ArrayList<>();
double selectCapacity = 0;
for(Item item : itemList){
if( (selectCapacity + item.weight) <= capacity){
selectCapacity = selectCapacity + item.weight;
Item selectItem = new Item(item.type,item.weight);
selectItemList.add(selectItem);
}else {
Item selectItem = new Item(item.type, capacity-selectCapacity);
selectItemList.add(selectItem);
break;
}
}
//选择结果输出
for (Item item : selectItemList){
System.out.println("选择了类型:"+ item.type+" 的物品,重量为:"+item.weight);
}
}
}
运行结果:
选择了类型:2 的物品,重量为:5.0
选择了类型:4 的物品,重量为:10.0
选择了类型:1 的物品,重量为:10.0
选择了类型:3 的物品,重量为:5.0
双指针算法
通过两个指针同时遍历数据结构(如数组或列表),以达到优化计算或处理的目的。
顺序双指针
指的是在遍历集合或数组时,使用两个指针以顺序(一个接一个)的方式遍历,通常用于解决包括两数之和在内的一系列问题。
JAVA 代码实现
实例:
两数之和的示例:找到一个数组中和为特定目标值的两个数的索引。
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class TwoSumTest {
public int[] twoSum(int[] nums, int target) {
//定义一个哈希表来存储已遍历数字的值和它们的索引
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
//计算每个数字所需的补数
int complement = target - nums[i];
//检查补数是否存在于哈希表中,如果找到了补数,返回两个数字的索引
if (map.containsKey(complement)) {
return new int[]{map.get(complement), i};
}
map.put(nums[i], i);
}
//如果遍历结束都没有找到答案,返回一个空数组,即无解
return new int[0];
}
public static void main(String[] args) {
TwoSumTest solver = new TwoSumTest();
int[] nums = {2, 7, 11, 15};
int target = 9;
int[] indices = solver.twoSum(nums, target);
System.out.println("Index of two numbers: " + Arrays.toString(indices));
}
}
运行结果:
Index of two numbers: [0, 1]
对撞双指针
指的是在遍历数组或链表时,两个指针以不同的速度移动,以解决某些问题,如查找循环/循环结束点,或者确定两个元素之间的距离。
具体实例:
-
求链表的中点:使用快慢指针,其中慢指针每次移动一步,快指针每次移动两步,当快指针到达链表末尾时,慢指针指向链表的中点。
ListNode findMiddle(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; } return slow; }
-
判断链表是否有环:使用快慢指针,如果链表有环,快慢指针会在环中相遇;如果没有环,快指针会先到达链表末尾。
boolean hasCycle(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; if (fast == slow) { return true; } } return false; }
-
求数组中的中位数:可以使用双指针,一个指针从数组开始向中间移动,另一个指针从数组末尾向中间移动。
double findMedianSortedArrays(int[] nums1, int[] nums2) { int m = nums1.length; int n = nums2.length; int left = -1, right = -1; int a = 0, b = 0; while (a < m || b < n) { if (left < right) { left = Math.max(left, a) == a ? nums1[a++] : left; right = Math.max(right, b) == b ? nums2[b++] : right; } else { left = Math.max(left, b) == b ? nums2[b++] : left; right = Math.max(right, a) == a ? nums1[a++] : right; } } return (left + right) / 2.0; }
JAVA 代码实现
实例:
两数之和的示例:找到一个数组中和为特定目标值的两个数的索引。
public class TwoPointersTest {
public static int[] twoSum(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
return new int[]{left, right};
} else if (sum < target) {
left++;
} else {
right--;
}
}
return new int[]{-1, -1}; // 如果没有找到符合条件的索引对,则返回-1
}
public static void main(String[] args) {
int[] nums = {2, 7, 11, 15};
int target = 9;
int[] indices = twoSum(nums, target);
System.out.println("数组中和为特定目标值的两个数的索引: " + indices[0] + ", " + indices[1]);
}
}
运行结果:
数组中和为特定目标值的两个数的索引: 0, 1
算法习题积累:
求解与数组相加的整数
现有两个整数数组 nums1 和 nums2。
从 nums1 中移除两个元素,并且所有其他元素都与变量 x 所表示的整数相加。如果 x 为负数,则表现为元素值的减少。
执行上述操作后,nums1 和 nums2 相等。当两个数组中包含相同的整数,并且这些整数出现的频次相同时,两个数组相等。
返回能够实现数组相等的 最小 整数 x。
JAVA 代码实现
生成测试用例的代码如下:
/**
* 生成容量为 arraySize 的 nums1,并根据 nums1 生成数组 nums2,且两个数组的值都在 0 ~ 1000 之间。
* */
public List<int[]> createArray(int arraySize){
//生成数组 nums1
List<int[]> numsList = new ArrayList<>();
//IntSupplier 函数式接口包含一个无参数的方法getAsInt,返回一个整数值
Stream<Integer> stream = Stream.generate(() -> ThreadLocalRandom.current().nextInt(0,1001)).limit(arraySize);
int[] nums1 = stream.mapToInt(i -> i.intValue()).toArray();
numsList.add(nums1);
//生成数组 nums2
int nums2[]=new int[nums1.length];
Random random = new Random();
int randomNum = random.nextInt(1000-getAarrayMax(nums1));
for (int i = 0; i < nums1.length; i++) {
nums2[i]=nums1[i]+randomNum;
}
//将 int 数组转化为 Integer 数组
Integer[] integers = Arrays.stream(nums2).boxed().toArray(Integer[]::new);
Set<Integer> integerSet = Arrays.stream(integers).skip(2).collect(Collectors.toSet());
nums2 = integerSet.stream().mapToInt(i -> i.intValue()).toArray();
numsList.add(nums2);
return numsList;
}
//获取数组的最大值
public int getAarrayMax(int[] arr){
int max = Arrays.stream(arr).max().getAsInt();
return max;
}
解法1:
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class AlgorithmTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入数组的长度以生成测试用例:");
int arraySize = scanner.nextInt();
if(arraySize >= 3 && arraySize <= 200){
AlgorithmTest algorithmTest = new AlgorithmTest();
List<int[]> numsList = algorithmTest.createArray(arraySize);
int[] nums1 = numsList.get(0);
int[] nums2 = numsList.get(1);
System.out.println("数组 nums1 为:" + Arrays.toString(nums1));
System.out.println("数组 nums2 为:" + Arrays.toString(nums2));
int m = nums1.length,n =nums2.length;
Arrays.sort(nums1);
Arrays.sort(nums2);
System.out.println("数组 nums1 升序后为:" + Arrays.toString(nums1));
System.out.println("数组 nums2 升序后为:" + Arrays.toString(nums2));
for (int i : new int[]{2,1,0}) {
int left = i + 1,right = 1;
while (left < m && right < n){
if(nums1[left] - nums2[right] == nums1[i] - nums2[0]){
++right;
}
++left;
}
if(right == n){
System.out.println("x 的值为:" + (nums2[0]-nums1[i]));
break;
}
}
}else {
System.out.println("输入的长度不符!");
}
}
}
解释:
首先将数组 nums1 和 nums2 中的元素都进行升序排序。记长度分别是 m 和 n,那么我们本质上需要找到一个长度为 n 的序列:
0 ≤ id0,id1 ... ,idn-1 < m
它是严格递增的,并且对于每一个 i(0 ≤ i < n),nums2[i] - nums1[idi] 的值都是相同的。在本题中,由于 m = n + 2,那么 id0 必然等于 {0,1,2} 其中的一个。这样一来,我们就可以对 id0 进行枚举了。
当我们确定了 id0 后,由于两个数组已经是有序的,我们就可以使用 双指针算法 得到 id1, ... ,idn-1。具体地,两个指针 left 和 right 分别指向 nums1 和 nums2 中的元素下标,它们的初始值分别为 id0 + 1 和 1。在双指针遍历的过程中,如果有:
nums1[left] - nums2[right] = nums1[id0] - nums2[0]
那么我们就找到了 idright = left ,我们可以将 left 和 right 均增加 1,否则只将 left 增加 1.
当 left 遍历完成后,如果 right = n,说明我们找到了 id0,id1, ... , idn-1 ,题目中需要求出 x 即为 nums2[0] - nums1[id0]。由于我们需要求出最小的 x ,而 nums1 是有序的,因此可以按照 {2,1,0} 的顺序枚举 id0 ,这样在找到 x 时一定得到的就是最小的 x。
运行结果:
请输入数组的长度以生成测试用例:
10
数组 nums1 为:[903, 911, 854, 573, 36, 74, 822, 338, 563, 648]
数组 nums2 为:[400, 625, 98, 916, 884, 710, 136, 635]
数组 nums1 升序后为:[36, 74, 338, 563, 573, 648, 822, 854, 903, 911]
数组 nums2 升序后为:[98, 136, 400, 625, 635, 710, 884, 916]
x 的值为:62