算法题
一、链表
1、反转一个单链表(206)
public static ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode node = null;
while (head != null) {
ListNode temp = head;
head = head.next;
temp.next = node;
node = temp;
}
return node;
}
2、给定一个链表,判断链表中是否有环(第二种好一些,141)
public static boolean hasCycle(ListNode head) { Set<ListNode> set = new HashSet<>(); while (head != null) { if (set.contains(head)) { return true; } else { set.add(head); } head = head.next; } return false; }
public static boolean hasCycle1(ListNode head) { if (head == null) { return false; } ListNode fast = head.next; ListNode slow = head; while (fast != null && slow != null && fast.next != null) { if (fast == slow) { return true; } slow = slow.next; fast = fast.next.next; } return false; }
3、将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的(21)
public static ListNode mergeTwoLists1(ListNode l1, ListNode l2) { if (l1 == null) { return l2; } if (l2 == null) { return l1; } ListNode head = null; ListNode pre = new ListNode(); while (l1 != null || l2 != null) { ListNode currentNode = new ListNode(); if (l1 == null) { pre.next = l2; break; } if (l2 == null) { pre.next = l1; break; } if (l1.val < l2.val) { currentNode.val = l1.val; l1 = l1.next; } else { currentNode.val = l2.val; l2 = l2.next; } if (head == null) { pre = currentNode; head = pre; } else { pre.next = currentNode; pre = pre.next; } } return head; }
public static ListNode mergeTwoLists(ListNode l1, ListNode l2) { if (l1 == null) { return l2; } if (l2 == null) { return l1; } if (l1.val < l2.val) { l1.next = mergeTwoLists(l1.next, l2); return l1; } else { l2.next = mergeTwoLists(l1, l2.next); return l2; } }
4、给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点(19)
public static ListNode removeNthFromEnd(ListNode head, int n) { ListNode sentry = new ListNode(-1); sentry.next = head; int length = 0; ListNode node = sentry; while (node != null) { ++length; node = node.next; } int x = 0; ListNode cNode = sentry; while (cNode != null) { if (n == (length - x - 1)) { cNode.next = cNode.next.next; return sentry.next; } ++x; cNode = cNode.next; } return head; }
public static ListNode removeNthFromEnd1(ListNode head, int n) { ListNode sentry = new ListNode(-1); sentry.next = head; ListNode pre = sentry; ListNode second = sentry; while (n >= 0) { second = second.next; --n; } while (second != null) { pre = pre.next; second = second.next; } pre.next = pre.next.next; return sentry.next; }
5、给定一个带有头结点 head 的非空单链表,返回链表的中间结点(876)
public static ListNode middleNode(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } return slow; }
二、排序
冒泡,插入,选择排序实现代码都非常简单,对于小规模数据的排序,用起来非常高效。但是在大规模数据排序的时候,这个时间复杂度还是稍微有点高,所以更倾向于使用时间复杂度为 O(nlogn) 的排序算法。
1、冒泡
/** * 冒泡排序 * 稳定排序、原地排序,时间复杂度,最好是o(n),最坏是o(n的平方),平均是o(n的平方) * * @param nums */ public static void bubbleSort(int[] nums) { int length = nums.length; if (length <= 1) { return; } for (int i = 0; i < length; i++) { boolean flag = true; for (int j = 1; j < length - i; j++) { if (nums[j] < nums[j - 1]) { int temp = nums[j]; nums[j] = nums[j - 1]; nums[j - 1] = temp; flag = false; } } if (flag) { return; } } }
2、插入排序
/**
* 插入排序
* 稳定排序、原地排序,时间复杂度,最好是o(n),最坏是o(n的平方),平均是o(n的平方)
* 冒泡和插入排序一般选择插入排序 原因:冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个
*
* @param nums
*/
public static void insertSort(int[] nums) {
int length = nums.length;
if (length <= 1) {
return;
}
for (int i = 1; i < length; i++) {
int j = i;
int value = nums[j];
for (; j > 0; j--) {
if (nums[j - 1] > value) {
nums[j] = nums[j - 1];
} else {
break;
}
}
nums[j] = value;
}
}
3、选择排序
/** * 选择排序 * 不稳定排序、原地排序,时间复杂度,最好是o(n的平方),最坏是o(n的平方),平均是o(n的平方) */ public static void selectionSort(int[] nums) { int n = nums.length; if (n <= 0) { return; } for (int i = 0; i < n - 1; i++) { int value = nums[i]; int index = i; for (int j = i; j < n - 1; j++) { if (nums[j + 1] < nums[i]) { nums[i] = nums[j + 1]; index = j + 1; } } nums[index] = value; } }
4、快速排序
/** * 快速排序 * 不稳定算法,是原地排序 * * @param nums */ public static void quictSort(int[] nums) { if (nums.length <= 1) { return; } quictSortC(nums, 0, nums.length - 1); } public static void quictSortC(int[] nums, int left, int right) { if (left >= right) { return; } int q = partition(nums, left, right); quictSortC(nums, left, q - 1); quictSortC(nums, q + 1, right); }
public static int partition(int[] nums, int left, int right) {
int value = nums[left];
int i = left;
int j = right;
while (i < j) {
while (i < j && nums[j] >= value) {
j--;
}
if (i < j) {
nums[i] = nums[j];
}
while (i < j && nums[i] < value) {
i++;
}
if (i < j) {
nums[j] = nums[i];
}
}
nums[i] = value;
return i;
}
5、归并排序
/** * 归并排序(分治思想)) * 最好、最坏、平均时间复杂度都是o(nlogn),不如快排常用的原因是不是原地排序算法 * 归并排序是稳定算法、不是原地排序,空间复杂度为o(n),因为每个子方法执行完空间就被回收了 * * @param nums */ public static void mergeSort(int[] nums) { if (nums.length <= 1) { return; } mergeSortC(nums, 0, nums.length - 1); } public static void mergeSortC(int[] nums, int min, int max) { if (min >= max) { return; } int p = (min + max) / 2; mergeSortC(nums, min, p); mergeSortC(nums, p + 1, max); merge(nums, min, p, max); } public static void merge(int[] data, int left, int center, int right) { // 临时数组 int[] tmpArr = new int[data.length]; // 右数组第一个元素索引 int mid = center + 1; // third 记录临时数组的索引 int third = left; // 缓存左数组第一个元素的索引 int tmp = left; while (left <= center && mid <= right) { // 从两个数组中取出最小的放入临时数组 if (data[left] <= data[mid]) { tmpArr[third++] = data[left++]; } else { tmpArr[third++] = data[mid++]; } } // 剩余部分依次放入临时数组(实际上两个while只会执行其中一个) while (mid <= right) { tmpArr[third++] = data[mid++]; } while (left <= center) { tmpArr[third++] = data[left++]; } // 将临时数组中的内容拷贝回原数组中 // (原left-right范围的内容被复制回原数组) while (tmp <= right) { data[tmp] = tmpArr[tmp++]; } }
6、利用快排查找第k大元素
//searchK为入口,时间复杂度O(n)
public static Integer searchK(Integer[] nums, int k){ if(nums.length ==0 || nums.length < k){ return null; } return searchC(nums, 0, nums.length-1, k); } private static Integer searchC(Integer[] arr, int l, int r, int n ){ int base = partition(l,r,arr); if(base == n-1){ //找到第n大元素直接返回 return arr[n-1]; }else if(base<n-1){ //在右半部分查找 return searchC(arr, base+1, r,n); }else{ //在左半部分查找 return searchC(arr, l, base-1,n); } } public static int partition(int s, int e, Integer[] nums) { int x = nums[s]; while (s < e) { while (s < e && nums[e] > x) { --e; } if (s < e) { nums[s] = nums[e]; } while (s < e && nums[s] <= x) { ++s; } if (s < e) { nums[e] = nums[s]; } } nums[s] = x; return s; }
7、堆排序
public static void swap(int[] nums, int i, int max){ int temp = nums[i]; nums[i] = nums[max]; nums[max] = temp; } //堆化 public static void heapify(int[] nums,int n, int i){ while(true){ int maxIndex = i; int leftIndex = (i<<1)+1; if(leftIndex < n && nums[leftIndex] > nums[i]){ maxIndex = leftIndex; } int rightIndex = (i+1)<<1; if(rightIndex < n && nums[rightIndex] > nums[maxIndex]){ maxIndex = rightIndex; } if(maxIndex == i){ break; } swap(nums, maxIndex, i); i = maxIndex; } } /** * 堆排序 * 1、对倒数第一个非叶子节点开始调整数组,使之成为一个大顶堆 * 2、堆顶和最后一个交换,对堆顶进行堆化,使0到n-1的数复合大顶堆 * 3、不断重复2过程 * * 整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn),所以,堆排序整体的时间复杂度是 O(nlogn)。堆排序不是稳定的排序算法 */ public static void heapSort(int[] nums) { if (nums.length <= 1) { return; } //建大顶堆 for (int i = nums.length / 2-1; i >= 0; --i) { heapify(nums, nums.length, i); } //排序 for(int i = nums.length-1;i>0;--i){ swap(nums, i, 0); heapify(nums, i, 0); } }
三、二分查找
1、查找等于k的在数组中的下标
/** * 二分查找 * 局限性: * 1、二分查找依赖的是顺序表结构,简单点说就是数组 * 2、二分查找针对的是有序数据,二分查找只能用在插入、删除操作不频繁,一次排序多次查找的场景,不然每次插入要维护有序性 * 3、数据量太小不适合二分查找,顺序遍历就好,不过数据之间的比较操作非常耗时,可用二分查找 * 4、数据量太大也不适合二分查找,因为数组要求连续空间,对内存要求高 * 二分查找虽然性能比较优秀,但应用场景也比较有限。底层必须依赖数组,并且还要求数据是有序的。对于较小规模的数据查找,我们直接使用顺序遍历就可以了,二分查找的优势并不明显。二分查找更适合处理静态数据,也就是没有频繁的数据插入、删除操作 * 时间复杂度 O(logn) * * @param nums * @param value */ public static int bsearch(int[] nums, int value) { if (nums.length == 0) { return -1; } int min = 0; int max = nums.length - 1; while (max >= min) { int mid = (min + ((max - min) >> 1)); if (value > nums[mid]) { min = mid + 1; } else if (value < nums[mid]) { max = mid - 1; } else { return mid; } } return -1; } /** * 二分查找递归 * * @param nums * @param left * @param rigth * @param value * @return */ public static int bsearch(int[] nums, int left, int rigth, int value) { if (left > rigth) { return -1; } int mid = left + ((rigth - left) >> 1); if (nums[mid] == value) { return mid; } else if (value > nums[mid]) { return bsearch(nums, mid + 1, rigth, value); } else { return bsearch(nums, left, mid - 1, value); } }
2、查找有序数组中第一个等于k的下标
public static int bsearch(int[] nums, int k) { if (nums.length == 0) { return -1; } int min = 0; int max = nums.length - 1; while (min <= max) { int mid = min + ((max - min) >> 1); if (nums[mid] < k) { min = mid + 1; } else { max = mid - 1; } } if (nums[min] == k) { return min; } return -1; } //方法2 public int bsearch(int[] a, int n, int value) { int low = 0; int high = n - 1; while (low <= high) { int mid = low + ((high - low) >> 1); if (a[mid] > value) { high = mid - 1; } else if (a[mid] < value) { low = mid + 1; } else { if ((mid == 0) || (a[mid - 1] != value)) return mid; else high = mid - 1; } } return -1; }
3、查找有序数组中最后一个等于k的下标
public int bsearch(int[] a, int n, int value) { int low = 0; int high = n - 1; while (low <= high) { int mid = low + ((high - low) >> 1); if (a[mid] > value) { high = mid - 1; } else if (a[mid] < value) { low = mid + 1; } else { if ((mid == n - 1) || (a[mid + 1] != value)) return mid; else low = mid + 1; } } return -1; }
4、查找第一个大于等于给定值的元素
public int bsearch(int[] a, int n, int value) { int low = 0; int high = n - 1; while (low <= high) { int mid = low + ((high - low) >> 1); if (a[mid] >= value) { if ((mid == 0) || (a[mid - 1] < value)) return mid; else high = mid - 1; } else { low = mid + 1; } } return -1; }
5、查找最后一个小于等于给定值的元素
public int bsearch7(int[] a, int n, int value) { int low = 0; int high = n - 1; while (low <= high) { int mid = low + ((high - low) >> 1); if (a[mid] > value) { high = mid - 1; } else { if ((mid == n - 1) || (a[mid + 1] > value)) return mid; else low = mid + 1; } } return -1; }
四、二叉树
public class Node { private Object data; //节点数据 private Node leftChild; //左子节点的引用 private Node rightChild; //右子节点的引用 //打印节点内容 public void display(){ System.out.println(data); } }
//中序遍历 public void infixOrder(Node current){ if(current != null){ infixOrder(current.leftChild); System.out.print(current.data+" "); infixOrder(current.rightChild); } } //前序遍历 public void preOrder(Node current){ if(current != null){ System.out.print(current.data+" "); preOrder(current.leftChild); preOrder(current.rightChild); } } //后序遍历 public void postOrder(Node current){ if(current != null){ postOrder(current.leftChild); postOrder(current.rightChild); System.out.print(current.data+" "); } }
//前序遍历非递归的方式 public void preOrder(Node root) { Stack<Node> stack = new Stack<Node>(); while (true) { while (root != null) { System.out.print(root.value + "\t"); stack.push(root); root = root.left; } if (stack.isEmpty()) { break; } root = stack.pop(); root = root.right; } } //中序遍历采用非递归的方式 public void inOrder(Node root) { Stack<Node> stack = new Stack<>(); while (true) { while (root != null) { stack.push(root); root = root.left; } if (stack.isEmpty()) { break; } root = stack.pop(); System.out.print(root.value + "\t"); root = root.right; } } //非递归后续遍历 public void postOrder(Node node) { Stack<Node> stack = new Stack<>(); Node cur, pre = null; //pre为额外结点,记录前一个结点,用于打印 stack.push(node); while (!stack.isEmpty()) { cur = stack.peek(); if ((cur.left == null && cur.right == null) || (pre != null && (cur.left == pre || cur.right == pre))) { //判断是否为上述条件决定是否打印 Node temp = stack.pop(); System.out.print(temp.value + " "); pre = temp; } else { if (cur.right != null) { //右结点入栈 stack.push(cur.right); } if (cur.left != null) { stack.push(cur.left); } } } }
五、八皇后问题(回溯算法)
int[] result = new int[8];//全局或成员变量,下标表示行,值表示queen存储在哪一列 public void cal8queens(int row) { // 调用方式:cal8queens(0); if (row == 8) { // 8个棋子都放置好了,打印结果 printQueens(result); return; // 8行棋子都放好了,已经没法再往下递归了,所以就return } for (int column = 0; column < 8; ++column) { // 每一行都有8中放法 if (isOk(row, column)) { // 有些放法不满足要求 result[row] = column; // 第row行的棋子放到了column列 cal8queens(row+1); // 考察下一行 } } } private boolean isOk(int row, int column) {//判断row行column列放置是否合适 int leftup = column - 1, rightup = column + 1; for (int i = row-1; i >= 0; --i) { // 逐行往上考察每一行 if (result[i] == column) return false; // 第i行的column列有棋子吗? if (leftup >= 0) { // 考察左上对角线:第i行leftup列有棋子吗? if (result[i] == leftup) return false; } if (rightup < 8) { // 考察右上对角线:第i行rightup列有棋子吗? if (result[i] == rightup) return false; } --leftup; ++rightup; } return true; } private void printQueens(int[] result) { // 打印出一个二维矩阵 for (int row = 0; row < 8; ++row) { for (int column = 0; column < 8; ++column) { if (result[row] == column) System.out.print("Q "); else System.out.print("* "); } System.out.println(); } System.out.println(); }
六、 0-1背包问题
题目:我们现在期望选择几件物品,装载到背包中。在不超过背包所能装载重量的前提下,如何让背包中物品的总重量最大?
1、回溯算法解法
private static int total = 0; //背包可以装的最大重量 /** * 回溯算法,其实就是穷举,列出来所有可能的结果,找出最优的解,时间复杂度是指数级别的 O(2^n),n为物品的数量 * 背包,总的承载重量是limitNum,现在有nums.length个物品,每个物品的重量不等,并且不可分割。 * * @param limitNum 背包限制的重量 * @param currentNum 表示当前已经装进去的物品的重量和 * @param nums 表示每个物品的重量 * @param n 表示考察到哪个物品了 */ public static void packageMax(int limitNum, int currentNum, int[] nums, int n) { if (currentNum == limitNum || n == nums.length) { //currentNum == limitNum 表示装满了, n == nums.length 表示所有物品都考察完了 if (currentNum > total) { total = currentNum; } return; } //不装物品进背包 packageMax(limitNum, currentNum, nums, n + 1); //装物品进背包 if ((nums[n] + currentNum) <= limitNum) { // 已经超过可以背包承受的重量的时候,就不要再装了 packageMax(limitNum, nums[n] + currentNum, nums, n + 1); } } public static void main(String[] args) { int[] nums = new int[]{8, 14, 50, 40, 80, 4, 15}; int k = knapsack(nums, 100, nums.length); System.out.println(k); }
2、回溯算法使用备忘录方式优化解法
static int limitNum = 100; //背包限制的重量 static int[] nums = new int[]{8, 14, 50, 40, 80, 4, 15}; //表示每个物品的重量 static boolean[][] status = new boolean[nums.length][limitNum + 1]; //计算过的物品备忘录 private static int total = 0; //背包可以装的最大重量 /** * 回溯算法,其实就是穷举,列出来所有可能的结果,找出最优的解,时间复杂度是指数级别的 O(2^n),n为物品的数量 * 使用备忘录方式优化,记录已经算过的状态,再遇到直接从算过的取,然后不重复计算 * 背包,总的承载重量是limitNum,现在有nums.length个物品,每个物品的重量不等,并且不可分割。 * 我们现在期望选择几件物品,装载到背包中。在不超过背包所能装载重量的前提下,如何让背包中物品的总重量最大? * * @param currentNum 表示当前已经装进去的物品的重量和 * @param n 表示考察到哪个物品了 */ public static void packageMax1(int currentNum, int n) { if (currentNum == limitNum || n == nums.length) { //currentNum == limitNum 表示装满了, n == nums.length 表示所有物品都考察完了 if (currentNum > total) { total = currentNum; } return; } if (status[n][currentNum]) { return; } status[n][currentNum] = true; //不装物品进背包 packageMax1(currentNum, n + 1); //装物品进背包 if ((nums[n] + currentNum) <= limitNum) { // 已经超过可以背包承受的重量的时候,就不要再装了 packageMax1(nums[n] + currentNum, n + 1); } } public static void main(String[] args) { packageMax1(0,0); System.out.println(total); }
3、动态规划解法
public static void main(String[] args) { int k = knapsack(nums, 100, nums.length); System.out.println(k); } /** * 动态规划,把求解分为n个阶段,每个阶段选择把物品放进背包和不放进背包,会有多个状态,合并每一阶段重复的状态,根据上一个阶段求出下一个阶段的值 * 最后一个阶段接近物品重量的值就是要求解的值 * 时间复杂度是 O(n*w),空间复杂度也是 O(n*w),动态规划是一种空间换时间的解决思路 * @param nums 表示每个物品的重量 * @param limitNum 背包限制的重量 * @param n 物品的数量 * @return */ public static int knapsack(int[] nums, int limitNum, int n) { boolean[][] status = new boolean[n][limitNum + 1]; status[0][0] = true;//第一行数据特殊出来,哨兵优化 if (nums[0] <= limitNum) { status[0][nums[0]] = true; } for (int i = 1; i < n; i++) { for (int j = 0; j <= limitNum; j++) { if (status[i - 1][j]) { status[i][j] = status[i - 1][j]; //与 status[i][j] = true 等价, 不放第i个物品进背包的状态 } } for (int j = 0; j <= limitNum - nums[i]; ++j) {//把第i个物品放入背包 if (status[i - 1][j] == true) { status[i][j + nums[i]] = true; } } } for (int i = limitNum; i >= 0; --i) { if (status[n - 1][i]) { return i; } } return 0; }
/** * 针对上一个使用二维数组动态规划的优化 */ public static int knapsack1(int[] nums, int limitNum, int n) { boolean[] status = new boolean[limitNum + 1]; status[0] = true;//第一行数据特殊出来,哨兵优化 if (nums[0] <= limitNum) { status[nums[0]] = true; } for (int i = 1; i < n; i++) { for (int j = limitNum - nums[i]; j >= 0; j--) {////把第i个物品放入背包 if (status[j] == true) { status[j + nums[i]] = true; } } } for (int i = limitNum; i >= 0; --i) { if (status[i]) { return i; } } return 0; }
七、0-1背包问题升级
问题描述:对于一组不同重量、不同价值、不可分割的物品,我们选择将某些物品装入背包,在满足背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少呢?
1、回溯算法实现
static int limitNum = 100; //背包限制的重量 static int[] nums = new int[]{8, 14, 50, 40, 80, 4, 15}; //表示每个物品的重量 static int[] values = new int[]{2, 5, 3, 7, 15, 18, 9}; //表示每个物品的价值 private static int total = 0; //背包可以装的重量的最大价值 /** * 回溯算法,其实就是穷举,列出来所有可能的结果,找出最优的解,时间复杂度是指数级别的 O(2^n),n为物品的数量 * 使用备忘录方式优化,记录已经算过的状态,再遇到直接从算过的取,然后不重复计算 * 背包,总的承载重量是limitNum,现在有nums.length个物品,每个物品的重量不等,并且不可分割。 * * @param currentNum 表示当前已经装进去的物品的重量和 * @param value 表示当前已经装进去的物品的重量的最大价值 * @param n 表示考察到哪个物品了 */ public static void packageMax1(int currentNum, int n, int value) { if (currentNum == limitNum || n == nums.length) { //currentNum == limitNum 表示装满了, n == nums.length 表示所有物品都考察完了 if (value > total) { total = value; } return; } //不装物品进背包 packageMax1(currentNum, n + 1, value); //装物品进背包 if ((nums[n] + currentNum) <= limitNum) { // 已经超过可以背包承受的重量的时候,就不要再装了 packageMax1(nums[n] + currentNum, n + 1, values[n] + value); } } public static void main(String[] args) { packageMax1(0, 0, 0); System.out.println(total); }
2、动态规划实现
public static void main(String[] args) { int[] nums = new int[]{8, 14, 50, 40, 80, 4, 15}; //表示每个物品的重量 int[] values = new int[]{2, 5, 3, 7, 15, 18, 9}; //表示每个物品的价值 int v = knapsack(nums, values, 100, nums.length); System.out.println(v); } /** * 动态规划,把求解分为n个阶段,每个阶段选择把物品放进背包和不放进背包,会有多个状态,合并每一阶段重复的状态(相同状态选择价值大的),根据上一个阶段求出下一个阶段的值 * 最后一个阶段接近物品重量的值就是要求解的值 * * @param nums 表示每个物品的重量 * @param values 表示每个物品的价值 * @param limitNum 背包限制的重量 * @param n 物品的数量 * @return */ public static int knapsack(int[] nums, int[] values, int limitNum, int n) { int[][] status = new int[n][limitNum + 1]; for (int i = 0; i < n; ++i) { // 初始化states for (int j = 0; j < limitNum + 1; ++j) { status[i][j] = -1; } } status[0][0] = 0; if (nums[0] <= limitNum) { status[0][nums[0]] = values[0]; } for (int i = 1; i < n; i++) { for (int j = 0; j <= limitNum; j++) { if (status[i - 1][j] >= 0) { status[i][j] = status[i - 1][j]; // 不放第i个物品进背包的状态 } } for (int j = 0; j <= limitNum - nums[i]; ++j) {//把第i个物品放入背包 if (status[i - 1][j] >= 0) { int v = status[i - 1][j] + values[i]; if (v > status[i][j + nums[i]]) { status[i][j + nums[i]] = v; } } } } int maxValue = -1; for (int i = 0; i <= limitNum; i++) { if (status[n - 1][i] > maxValue) { maxValue = status[n - 1][i]; } } return maxValue; }
八、购物车问题
淘宝的“双十一”购物节有各种促销活动,比如“满 200 元减 50 元”。假设你女朋友的购物车中有 n 个(n>100)想买的商品,她希望从里面选几个,在凑够满减条件的前提下,让选出来的商品价格总和最大程度地接近满减条件(200 元),这样就可以极大限度地“薅羊毛”
/** * 动态规划 * 假设你女朋友的购物车中有 n 个(n>100)想买的商品,她希望从里面选几个,在凑够满减条件的前提下,让选出来的商品价格总和最大程度地接近满减条件(200 元),这样就可以极大限度地“薅羊毛” * 要找到凑单大于200的最小值的所有商品,这个可凑到的额度选为200*3,如果超过这个金额没有凑单的必要 * * @param nums 表示每个商品的价格 * @param limitNum 要凑单的满减额度 * @param n 物品的数量 * @return */ public static void order(int[] nums, int limitNum, int n) { boolean[][] status = new boolean[n][3 * limitNum + 1]; status[0][0] = true;//第一行数据特殊出来,哨兵优化 if (nums[0] <= 3 * limitNum) { status[0][nums[0]] = true; } for (int i = 1; i < n; i++) { for (int j = 0; j <= 3 * limitNum; j++) { if (status[i - 1][j]) { status[i][j] = status[i - 1][j]; //与 status[i][j] = true 等价, 不放第i个物品进背包的状态 } } for (int j = 0; j <= 3 * limitNum - nums[i]; ++j) {//把第i个物品放入背包 if (status[i - 1][j] == true) { status[i][j + nums[i]] = true; } } } int i;//凑单最小的金额 for (i = limitNum; i <= 3 * limitNum; i++) { if (status[n - 1][i]) { break; } } if (i == 3 * limitNum + 1) {//没有凑到 return; } for (int j = n - 1; j >= 1; j--) { if ((i - nums[j]) >= 0 && status[j - 1][i - nums[j]]) { System.out.println(nums[j]); //购买了这个商品 i = i - nums[j]; } } if (i != 0) { System.out.println(nums[0]); } }
九、矩阵问题
题目描述:有一个 n 乘以 n 的矩阵 w[n][n]。矩阵存储的都是正整数。棋子起始位置在左上角,终止位置在右下角。我们将棋子从左上角移动到右下角。每次只能向右或者向下移动一位。从左上角到右下角,会有很多不同的路径可以走。我们把每条路径经过的数字加起来看作路径的长度。那从左上角移动到右下角的最短路径长度是多少呢?
1、回溯算法实现
static int minDist = Integer.MAX_VALUE;//最短距离 /** * 回溯算法 * @param i 矩阵行 * @param j 矩阵列 * @param dist 计算出来的路径 * @param nums 矩阵 * @param n 矩阵的边界 */ public static void minDistBT(int i, int j, int dist, int[][] nums, int n) { dist += nums[i][j]; if (i == n-1 && j == n-1) { if (dist < minDist) { minDist = dist; } return; } if (j < n-1) { //往下走 minDistBT(i, j + 1, dist, nums, n); } if (i < n-1) { //往右走 minDistBT(i + 1, j, dist, nums, n); } } public static void main(String[] args) { int[][] nums = new int[][]{{1, 3, 5, 9}, {2, 1, 3, 4}, {5, 2, 6, 7}, {6, 8, 4, 3}}; minDistBT(0, 0, 0, nums, 4); System.out.println(minDist); }
1、动态规划--状态转移法
public static void main(String[] args) { int[][] nums = new int[][]{{1, 3, 5, 9}, {2, 1, 3, 4}, {5, 2, 6, 7}, {6, 8, 4, 3}}; System.out.println(minDistDP(nums, nums.length)); } /** * 动态规划--状态转移法 * @param matrix * @param n * @return */ public static int minDistDP(int[][] matrix, int n) { int[][] status = new int[n][n]; status[0][0] = matrix[0][0]; for (int i = 1; i < n; i++) { //初始化第一行 status[0][i] = status[0][i - 1] + matrix[0][i]; } for (int i = 1; i < n; i++) { //初始化第一列 status[i][0] = status[i - 1][0] + matrix[i][0]; } for (int i = 1; i < n; i++) { //计算每一个矩阵的值 for (int j = 1; j < n; j++) { int min = status[i - 1][j] > status[i][j - 1] ? status[i][j - 1] : status[i - 1][j]; status[i][j] = matrix[i][j] + min; } } return status[n - 1][n - 1]; }
2、动态规划--状态转移方程法
private static int[][] nums = {{1, 3, 5, 9}, {2, 1, 3, 4}, {5, 2, 6, 7}, {6, 8, 4, 3}}; //矩阵 private static int n = nums.length; //n*n矩阵 private static int[][] mem = new int[n][n]; //推导出来的值 public static int minDistDP1(int i, int j) { if (i == 0 && j == 0) { return nums[i][j]; } if (mem[i][j] > 0) { return mem[i][j]; } int up = Integer.MAX_VALUE; if (i > 0) { up = minDistDP1(i - 1, j); } int left = Integer.MAX_VALUE; if (j > 0) { left = minDistDP1(i, j - 1); } int min = up > left ? left : up; mem[i][j] = nums[i][j] + min; return mem[i][j]; } public static void main(String[] args) { System.out.println(minDistDP1(n - 1, n - 1)); }

浙公网安备 33010602011771号