From fb9949eb70b2f5b194f4f9ad6861c9fe3c04ef73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=B5=8E=E8=8A=9D?= Date: Sat, 23 Nov 2024 15:58:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(all):=20=E5=AE=8C=E5=96=84=E6=96=87?= =?UTF-8?q?=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...27\346\263\225\350\257\246\350\247\243.md" | 659 +++++++++------ .../java/javaessay/java-algorithms/index.html | 786 +++++++++++------- .../java/javaessay/java-algorithms/index.html | 786 +++++++++++------- 3 files changed, 1329 insertions(+), 902 deletions(-) diff --git "a/blog-site/content/posts/java/javaessay/\347\256\227\346\263\225\350\257\246\350\247\243.md" "b/blog-site/content/posts/java/javaessay/\347\256\227\346\263\225\350\257\246\350\247\243.md" index f6fb60a1..785afbf6 100644 --- "a/blog-site/content/posts/java/javaessay/\347\256\227\346\263\225\350\257\246\350\247\243.md" +++ "b/blog-site/content/posts/java/javaessay/\347\256\227\346\263\225\350\257\246\350\247\243.md" @@ -212,6 +212,140 @@ class EightQueens { } ``` +### 骑士周游问题 +骑士周游问题也称马踏棋盘算法是一个经典的回溯问题。将骑士随机放在国际象棋的8×8棋盘0~7的某个方格中,骑士按走棋规则进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。 + +该算法通过回溯的方式在棋盘上递归地寻找可能的路径。每一步,骑士根据棋盘大小和当前的位置选择一个合法的目标格子。每次跳跃后,记录骑士已经访问过的格子,直到骑士成功遍历所有格子或发现无法继续前进。若骑士在某一时刻无法继续走,则回溯到上一步,尝试其他未访问过的格子。 + +骑士周游问题实际上是图的深度优先搜索的应用,适用于需要解决路径遍历和组合优化的问题。尽管在实际应用中较少见,但它对学习回溯算法和图搜索算法非常有帮助。 +```java +public class HouseChessBoardDemo { + public static void main(String[] args) { + HouseChessBoard houseChessBoard = new HouseChessBoard(7, 7, 2, 4); + houseChessBoard.showChessBoard(); + } +} + +class HouseChessBoard { + + /** + * 表示棋盘的列 + */ + private int x; + /** + * 表示棋盘的行 + */ + private int y; + /** + * 创建一个数组,标记棋盘的各个位置是否被访问过,true表示已经访问过 + */ + private boolean visited[]; + /** + * 使用一个属性,标记是否棋盘的所有位置都被访问 如果为true,表示成功 + */ + private boolean finished; + + private int[][] chessboard; + + public HouseChessBoard(int x, int y, int row, int column) { + this.x = x; + this.y = y; + this.visited = new boolean[x * y]; + this.chessboard = new int[x][y]; + traversalChess(this.chessboard, row-1, column-1, 1); + } + + public void showChessBoard(){ + for (int[] ints : this.chessboard) { + for (int anInt : ints) { + System.out.print(anInt + "\t"); + } + System.out.println(); + } + } + + public void traversalChess(int[][] chessboard, int row, int column, int step) { + chessboard[row][column] = step; + + // 将当前位置标记为已经访问过 + visited[row * x + column] = true; + + // 获取当前位置的下一个可走通的位置的集合 + ArrayList nextPos = getNext(new Point(column, row)); + + sort(nextPos); + + while (nextPos.size() > 0) { + // 获取当前可走通的位置 + Point current = nextPos.remove(0); + // 判断当前该点是否被访问过,如果没有被访问过则继续向下访问 + if (!visited[current.y * x + current.x]) { + traversalChess(chessboard, current.y, current.x, step + 1); + } + } + + // 当遍历完可走的位置集合后,如果发现该路不通,则进行回溯,否则标记为完成 + if (step < x * y && !finished) { + chessboard[row][column] = 0; + visited[row * x + column] = false; + } else { + finished = true; + } + } + + // 将可走通路根据回溯次数进行从小到大排序 + public void sort(ArrayList points){ + points.sort((o1, o2) -> { + int next1 = getNext(o1).size(); + int next2 = getNext(o2).size(); + return next1 - next2; + }); + } + + + + // 获取下一个可走的位置 + public ArrayList getNext(Point current) { + ArrayList ps = new ArrayList(); + Point p1 = new Point(); + // 表示马儿可以走5这个位置 + if ((p1.x = current.x - 2) >= 0 && (p1.y = current.y - 1) >= 0) { + ps.add(new Point(p1)); + } + // 判断马儿可以走6这个位置 + if ((p1.x = current.x - 1) >= 0 && (p1.y = current.y - 2) >= 0) { + ps.add(new Point(p1)); + } + // 判断马儿可以走7这个位置 + if ((p1.x = current.x + 1) < x && (p1.y = current.y - 2) >= 0) { + ps.add(new Point(p1)); + } + // 判断马儿可以走0这个位置 + if ((p1.x = current.x + 2) < x && (p1.y = current.y - 1) >= 0) { + ps.add(new Point(p1)); + } + // 判断马儿可以走1这个位置 + if ((p1.x = current.x + 2) < x && (p1.y = current.y + 1) < y) { + ps.add(new Point(p1)); + } + // 判断马儿可以走2这个位置 + if ((p1.x = current.x + 1) < x && (p1.y = current.y + 2) < y) { + ps.add(new Point(p1)); + } + // 判断马儿可以走3这个位置 + if ((p1.x = current.x - 1) >= 0 && (p1.y = current.y + 2) < y) { + ps.add(new Point(p1)); + } + // 判断马儿可以走4这个位置 + if ((p1.x = current.x - 2) >= 0 && (p1.y = current.y + 1) < y) { + ps.add(new Point(p1)); + } + return ps; + } + +} +``` + ## 排序算法 排序算法是一类用于将数据集合按照某种顺序排列的算法。排序是计算机科学中非常基础且常见的任务,广泛应用于各类应用场景,如数据库、搜索引擎等。根据算法的不同实现方式,排序算法可以分为多种类型,常见的排序算法包括交换排序、选择排序、插入排序、归并排序、快速排序等。 @@ -288,7 +422,7 @@ class BubbleSorting { 选择排序的时间复杂度为 O(n²),对于小规模数据排序时,选择排序实现简单,且无需额外的空间。它是原地排序算法,意味着它不会占用额外的内存空间(除去输入数组外)。 -尽管选择排序的空间复杂度较低,但时间复杂度为 O(n²),当数据量较大时,效率较低。即使数组已经部分有序,选择排序仍然需要进行相同次数的比较和交换,导致它的性能较差。此外,选择排序不稳定,即相等元素的顺序可能会发生改变。 +尽管选择排序的空间复杂度较低,但时间复杂度为 O(n²),当数据量较大时,效率较低。即使数组已经部分有序,选择排序仍然需要进行相同次数的比较和交换,导致它的性能较差。选择排序不稳定,即相等元素的顺序可能会发生改变。 选择排序适用于数据量较小的场景,特别是在对内存空间要求较高的情况下(例如,只有少量的额外内存可用)。它通常用于教学和理解排序算法的基本原理。在实际应用中,由于效率较低,选择排序很少用于大规模数据的排序。 @@ -636,7 +770,7 @@ class HeapSorting { 基数排序的时间复杂度为 O(nk),其中 n 是数据量,k 是数据中位数的长度。与其他排序算法相比,基数排序在处理大数据量时能展现出较好的性能,特别适用于排序位数较少的整数或字符串。由于不涉及比较,基数排序在特定场景下比比较排序算法更为高效。 -基数排序的缺点是需要额外的存储空间来存储排序过程中的数据。此外,它只适用于特定类型的数据,如整数或固定长度的字符串,对其他类型的数据不适用。基数排序的时间复杂度 O(nk) 可能在 k 较大的情况下,性能不如 O(n log n) 的比较排序算法。 +基数排序的缺点是需要额外的存储空间来存储排序过程中的数据。它只适用于特定类型的数据,如整数或固定长度的字符串,对其他类型的数据不适用。基数排序的时间复杂度 O(nk) 可能在 k 较大的情况下,性能不如 O(n log n) 的比较排序算法。 基数排序适合处理大量的整数或字符串,尤其是当数据的位数较少时。它广泛应用于对大规模数字数据进行排序的场景,比如处理电话号码、邮政编码等。基数排序还适用于需要保持数据稳定性的排序任务,如多关键字排序。 @@ -690,10 +824,16 @@ class BucketSorting { ``` ## 查找算法 -[//]: # (写到了这里) +查找算法是用于在数据集合中快速定位目标元素的一类算法。它的核心目的是根据特定的查询条件在数据结构中查找目标位置或验证目标是否存在。查找算法的效率通常与数据集合的存储方式以及数据的有序程度密切相关。 +常见的查找算法包括线性查找、二分查找、哈希查找和树形查找等。 ### 线性查找 -线性查找又称顺序查找,是一种最简单的查找方法,它的基本思想是从第一个记录开始,逐个比较记录的关键字,直到和给定的K值相等,则查找成功;若比较结果与文件中n个记录的关键字都不等,则查找失败。 +线性查找是一种基础的查找算法,它从数据集合的第一个元素开始,依次检查每个元素是否符合目标条件,直到找到目标元素或遍历完整个集合。适用于无序或有序的线性数据结构,如数组和链表,使用时不需要对数据进行预处理。 + +线性查找实现简单,逻辑清晰,代码容易编写和维护。它适用于任何类型的线性数据集合,无需对数据排序或建立额外的索引。同时,它在原数据集合上操作,不需要额外的存储空间,空间复杂度为 O(1)。 +但是线性查找效率较低,时间复杂度为 O(n),需要逐个比较数据集合中的元素,查找效率会随着数据规模的增大而显著下降。即使是有序的数据集合,也无法跳过部分元素进行优化。 + +线性查找常用于小规模数据集合或无序数据。当查找任务简单,或者数据存储在链表等只能线性访问的结构中时,线性查找是一种直接有效的选择。 ```java class LinearSearch{ @@ -711,11 +851,14 @@ class LinearSearch{ ``` ### 二分查找 -二分查找也称折半查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。 +二分查找是一种高效的查找算法,用于在有序数据集合中定位目标元素的位置。它通过每次将查找范围缩小为原来的一半,逐步逼近目标值,直到找到目标元素或查找范围为空为止。适用于数组等支持随机访问的有序数据结构。 + +二分查找的时间复杂度为 O(log n),相比线性查找,效率更高,尤其在数据规模较大时表现优异。它的逻辑清晰、实现简单,且无需占用额外的存储空间,空间复杂度为 O(1)。 +但二分查找仅适用于有序数据,若数据无序,需要先进行排序,增加了时间开销。同时,它依赖于随机访问的数据结构,例如数组,不适用于链表等线性访问的数据结构。 -**二分查找算法的前提,数组必须是有序数组,如果没有有序列表,请使用[排序算法](/iblog/posts/essays/data-structures-algorithms/#排序算法)对列表进行排序。** +二分查找常用于需要在有序数据集合中快速查找元素的场景,例如数组中定位某个值的位置。在解决与查找相关的问题时,如在字典、数据库索引或有序数组中查找特定值时,二分查找是一种有效的选择。 -递归实现二分查找 +二分查找算法的前提,数组必须是有序数组,递归实现二分查找: ```java class BinarySearch { @@ -782,12 +925,14 @@ class BinarySearch { ``` ### 插值查找 -插值查找算法类似于二分查找,与二分查找不同的是插值查找每次从自适应 mid 处开始查找,而不是像二分查找那样每次都从中间开始找。 +插值查找是一种改进的查找算法,适用于在有序数据集合中查找目标元素。它根据目标值与当前查找区间的上下界的相对位置,动态计算出一个可能的查找位置,从而缩小查找范围。插值查找的核心思想是通过插值公式预测目标值的位置,类似于在字典中查找单词。 -![数据结构与算法-021](/iblog/posts/annex/images/essays/数据结构与算法-021.png) +插值查找在数据分布较为均匀时性能优异,查找时间复杂度可以接近 O(log log n)。相比二分查找,它减少了不必要的比较次数,尤其在数据量大且分布均匀的情况下表现更加高效。 +但插值查找对数据分布有较高要求,只有在数据分布均匀时才能发挥优势。当数据分布不均时,插值公式可能会产生不准确的预测,导致性能退化至 O(n)。它仅适用于支持随机访问的有序数据结构。 -注意:对于数据量较大,关键字分布比较均匀(最好是线性分布)的查找表来说,采用插值查找,速度较快;对于关键字分布不均匀的情况下,该方法不一定比二分查找要好。 +所以插值查找适用于分布均匀且规模较大的有序数据集合。例如在处理数据库索引、统计数据或大规模有序数组时,如果数据分布均匀,插值查找是一种高效的选择。 +注意对于数据量较大,关键字分布比较均匀(最好是线性分布)的查找表来说,采用插值查找,速度较快。对于关键字分布不均匀的情况下,该方法不一定比二分查找要好。 ```java class InsertValueSearch { @@ -815,13 +960,14 @@ class InsertValueSearch { ``` ### 斐波那契查找 -斐波那契查找是基于【黄金分割】的二分查找。即在斐波那契队列中,将二分查找中的分割点替换为黄金分割点,来查找。 +斐波那契查找是一种基于斐波那契数列的查找算法,用于在有序数据集合中查找目标元素。该算法通过利用斐波那契数列将查找区间划分为黄金分割比例,来更接近目标值的方式逐步缩小范围,从而提高查找效率。 + +> 黄金分割是指将整体一分为二,较大部分与整体部分的比值等于较小部分与较大部分的比值,其比值约为0.618。这个比例被公认为是最能引起美感的比例,因此被称为黄金分割。 -> 黄金分割是指将整体一分为二,较大部分与整体部分的比值等于较小部分与较大部分的比值,其 比值 约为 0.618。这个比例被公认为是最能引起美感的 比例,因此被称为黄金分割。 +斐波那契查找在处理较大规模的有序数据时性能较好,时间复杂度为 O(log n)。相比二分查找,它可以减少加法运算,因为只需要使用减法和数组索引计算,适合某些特定场景。 +但是斐波那契查找要求数据集合的长度接近某个斐波那契数,若长度不匹配,需要对数据进行填充。与二分查找相比,其实现较为复杂,且对数据分布的适应性稍差。 -斐波那契查找特点: -- 平均性能「斐波那契查找」好于「二分查找」; -- 「斐波那契查找」计算 mid 的时候 使用加减法而不是除法,会微弱提升效率; +适用于大规模、数据有序的集合,尤其是在硬件资源有限或需要减少加法运算的情况下。它在数据库查找、索引优化等特定环境中有一定应用价值。 ```java class FibonacciSearch{ @@ -858,12 +1004,119 @@ class FibonacciSearch{ } ``` +### 哈希查找 +哈希查找是一种基于散列技术的查找方法,通过将关键字映射到哈希表中的地址,实现快速定位目标元素。它使用哈希函数将输入数据转化为表中的索引值,避免了逐一遍历的低效过程。 + +哈希查找的查找效率高,平均时间复杂度为 O(1),尤其适用于查找频繁的大数据集合。它的插入、删除操作也非常高效,适合动态更新的场景。 +但哈希查找依赖于设计合理的哈希函数和足够大的哈希表空间。若哈希函数不均匀,可能出现大量冲突,影响查找效率。在解决冲突时,链地址法或开放地址法可能增加复杂度。哈希查找无法直接支持范围查询。 + +哈希查找广泛应用于需要快速查找的场景,例如数据库索引、缓存(如 Redis)、散列表集合和字典等。在需要高效键值对操作的情况下,哈希查找是一种常用选择。 + +```java +public class HashTable { + private static final int SIZE = 10; // 哈希表大小 + private LinkedList>[] table; // 链地址法存储 + + // 构造方法 + @SuppressWarnings("unchecked") + public HashTable() { + table = new LinkedList[SIZE]; + for (int i = 0; i < SIZE; i++) { + table[i] = new LinkedList<>(); + } + } + + // 哈希函数 + private int hash(K key) { + return Math.abs(key.hashCode()) % SIZE; + } + + // 插入键值对 + public void put(K key, V value) { + int index = hash(key); + LinkedList> bucket = table[index]; + + for (Entry entry : bucket) { + if (entry.key.equals(key)) { + entry.value = value; // 如果键已存在,更新值 + return; + } + } + + bucket.add(new Entry<>(key, value)); // 插入新键值对 + } + + // 查找值 + public V get(K key) { + int index = hash(key); + LinkedList> bucket = table[index]; + + for (Entry entry : bucket) { + if (entry.key.equals(key)) { + return entry.value; + } + } + + return null; // 未找到 + } + + // 删除键值对 + public void remove(K key) { + int index = hash(key); + LinkedList> bucket = table[index]; + + bucket.removeIf(entry -> entry.key.equals(key)); + } + + // 哈希表中的键值对 + private static class Entry { + K key; + V value; + + Entry(K key, V value) { + this.key = key; + this.value = value; + } + } + + // 打印哈希表内容 + public void printHashTable() { + for (int i = 0; i < SIZE; i++) { + System.out.print("Bucket " + i + ": "); + for (Entry entry : table[i]) { + System.out.print("[" + entry.key + ": " + entry.value + "] "); + } + System.out.println(); + } + } + + // 测试 + public static void main(String[] args) { + HashTable hashTable = new HashTable<>(); + hashTable.put("Alice", 25); + hashTable.put("Bob", 30); + hashTable.put("Charlie", 35); + + System.out.println("Alice's age: " + hashTable.get("Alice")); // 输出: Alice's age: 25 + System.out.println("Bob's age: " + hashTable.get("Bob")); // 输出: Bob's age: 30 + + hashTable.remove("Alice"); + System.out.println("After removal, Alice's age: " + hashTable.get("Alice")); // 输出: After removal, Alice's age: null + + hashTable.printHashTable(); + } +} +``` + ## 哈夫曼编码 -赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法。 +哈夫曼编码是一种常见的数据压缩算法,用于在编码过程中以最小的空间表示数据。它根据字符出现的频率为每个字符分配不同长度的编码,频率高的字符使用较短的编码,频率低的字符使用较长的编码。通过这种方式,哈夫曼编码能够大大减少数据存储和传输所需的空间,特别适合用于文本数据的压缩。 + +哈夫曼编码是变长编码。它为不同的字符分配不同长度的编码,频率较高的字符使用较短的编码,频率较低的字符使用较长的编码。通过这种方式,哈夫曼编码能够有效减少存储空间,尤其在字符频率分布不均的情况下。它是一种前缀编码,没有任何编码是另一个编码的前缀,这样可以避免解码过程中出现歧义。 +主要缺点是它对于频率分布均匀的数据效果不明显。当字符频率接近时,哈夫曼编码无法有效减少空间开销。另一个缺点是,哈夫曼编码需要在解码时依赖树的结构,可能导致对动态数据流的支持不够灵活。 -哈夫曼编码是哈夫曼树在电讯通信中的经典的应用之一。哈夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间。哈夫曼码是可变字长编码的一种。Huffman于1952年提出一种编码方法,称之为最佳编码。 +哈夫曼编码广泛应用于需要数据压缩的场景,其压缩率通常在20%~90%之间,如文本文件压缩、图像格式(如 JPEG)压缩、视频压缩以及文件传输等。它适用于字符频率分布不均的情况,可以显著减少存储和传输的空间需求。 ->定长编码与变长编码,以字符串like like为例: +>定长编码与变长编码,以字符串“like like”为例: >- 定长编码: >1. 将上述字符串转换对应的ASCII: 108 105 107 101 32 108 105 107 101 >2. ASCII转换为二进制:01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 @@ -874,13 +1127,11 @@ class FibonacciSearch{ >3. 最终转换为变长编码为:011011100011011 上述的变长编码 011011100011011 在解码的时候会出现多意现象,比如当匹配到数字1,是把1解成i还是按照10来进行解码。因为这种现象的存在,所以在进行变长编码时,编码要符合前缀编码。 -> 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码。 +> 字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码,即不能匹配到重复的编码。 -构建哈夫曼编码思路: -1. 统计字节数组中各个数据的权重,即字符出现的次数; -2. 将统计好的字符出现的次数,[构建成哈夫曼树](/iblog/posts/essays/data-structures-algorithms/#哈夫曼树); -3. 根据上面创建的哈夫曼树获得每个数值对应的可变长编码值,规定结点的左边为0 ,右边为1; -4. 以每个数值新的编码重新对字符数组进行编码,即可得到赫夫曼编码后的值; +哈夫曼编码的工作原理主要包括两个步骤: +1. 构建哈夫曼树:首先,根据字符出现的频率构建一棵哈夫曼树。该树是通过将频率最低的两个节点合并生成新的节点,一直到所有节点合并为一个根节点。节点的权重是字符的频率。 +2. 生成编码:通过遍历哈夫曼树从根到叶子的路径来为每个字符生成唯一的二进制编码。路径上的左子树分配“0”,右子树分配“1”。 假如一段信息里只有A,B,C,D,E,F这6个字符,他们出现的次数依次是2次,3次,7次,9次,18次,25次,最终构建成哈夫曼编树为下图所示: @@ -891,7 +1142,7 @@ class FibonacciSearch{ A=11100 B=11101 C=1111 D=110 E=10 F=0 ``` -利用哈夫曼编码,压缩解压文件: +利用哈夫曼编码,压缩解压文件示例代码: ```java public class HuffmanCodeTest { @@ -1211,14 +1462,15 @@ class Node implements Comparable { ``` ## 分治算法 -分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)…… +分治算法是一种算法设计策略,其基本思想是将一个复杂的问题分解为多个规模较小的子问题,递归地解决这些子问题,再将它们的结果合并起来,最终得到原问题的解。分治法通常适用于可以被分解为相似子问题的情况,并且每个子问题的解可以组合成原问题的解。 -分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。 +分治算法能有效地简化复杂问题,通过将问题分解为较小的、易于处理的子问题,既能提高解决问题的效率,又能使问题的结构更加清晰。对于一些特定问题,分治算法还可以利用并行处理,从而进一步优化性能。 +缺点在于递归调用可能带来额外的栈空间开销,而且在一些问题中,子问题可能会重复计算,浪费计算资源。这种重复计算的问题可以通过动态规划等技术加以优化。 -使用分治算饭解决汉诺塔问题 +分治算法适用于可以将问题分解为若干个相似子问题的场景,常见的应用包括排序算法(如归并排序、快速排序)、查找算法(如二分查找)以及某些数学运算(如矩阵乘法、计算几何问题)。它尤其适合需要递归求解且能通过合并子问题的解得到最终答案的问题。 +使用分治算法解决汉诺塔问题: > 汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。 - ```java public class HanoiTowerDemo { public static void main(String[] args) { @@ -1251,20 +1503,21 @@ class HanoiTower { ``` ## 动态规划算法 -动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。 +动态规划是一种解决最优化问题的算法设计方法。它将问题分解为多个相互重叠的子问题,保存每个子问题的解,从而避免重复计算。这种方法通常用于具有重叠子问题和最优子结构特征的问题,通过将子问题的解存储起来,显著提高效率。 +动态规划的基本思想是通过将问题分解为更小的子问题,逐步解决这些子问题,并通过存储中间结果来避免重复计算。常见的动态规划实现有两种方式:自顶向下方式(递归并通过记忆化存储子问题结果)和自底向上方式(通过迭代解决所有子问题,从最小的子问题开始构造解)。最终所有子问题的解结合起来得到原问题的解。 + +动态规划通过存储已解决子问题的结果来避免重复计算,从而提升了计算效率。它特别适合处理那些可以被拆解成重叠子问题的最优化问题,且能找到全局最优解。动态规划提供了一种系统化的求解方式,使得问题的结构更加清晰和可管理。 +主要缺点是空间复杂度较高,尤其在问题的状态空间较大时,需要大量存储中间结果,消耗更多内存。动态规划的设计也可能比较复杂,需要仔细识别问题的最优子结构和重叠子问题,尤其在某些问题中,构建状态转移方程可能不容易。 -动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 ) +动态规划广泛应用于具有最优子结构和重叠子问题的问题,常见的应用场景包括求解最短路径问题、背包问题、最长公共子序列、矩阵链乘法等。这些问题通常可以通过递归分解为多个较小的子问题,且每个子问题的解都对最终解有影响。动态规划能在这些场景中找到最优解,并避免重复计算,提高效率。 -关于动态规划最经典的问题当属背包问题。 -> 背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。 ->| 物品 | 重量 | 价格 | +关于动态规划最经典的问题当属背包问题。背包问题是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。 +| 物品 | 重量 | 价格 | |-----|-----|-----| | 吉他(G) | 1 | 1500 | | 音响(S) | 4 | 3000 | | 电脑(L) | 3 | 2000 | - - ```java public class KnapsackProblemDemo { public static void main(String[] args) { @@ -1323,82 +1576,90 @@ class KnapsackProblem { ``` ## KMP算法 -> KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。 +KMP(Knuth-Morris-Pratt)算法是一种用于字符串匹配的高效算法,通过预处理模式串构建部分匹配表,避免重复比较,从而提高字符串匹配效率。与传统的暴力匹配算法不同,KMP算法通过部分匹配表指导模式串的跳跃,减少了不必要的字符比较,其时间复杂度为 O(n + m),其中 n 是文本串的长度,m 是模式串的长度。 + +KMP算法的核心原理是利用模式串的部分匹配信息来避免重复比较。当字符不匹配时,算法通过已构建的部分匹配表跳过不必要的字符,而不是从头开始重新比较。部分匹配表记录模式串中每个位置的最长前后缀相同的长度,在发生不匹配时,算法根据该表迅速调整模式串的位置,继续匹配剩余的部分,从而提高效率。 -- 常规算法匹配字符串 +KMP算法能够高效地进行字符串匹配,时间复杂度为 O(n + m),远低于暴力匹配算法的 O(n * m)。它通过预处理模式串,避免了对文本串中已匹配部分的重复比较,显著提高了匹配的速度,特别适合于长文本串的多次匹配。 +缺点在于预处理阶段需要计算模式串的部分匹配表,这增加了实现的复杂度,特别是在模式串较短或匹配次数较少时,预处理开销可能不值得。对于初学者来说,理解部分匹配表的构建过程及其在匹配中的应用可能较为复杂。 +适用于需要进行高效字符串匹配的场景,特别是在长文本串与模式串多次匹配时。它广泛应用于文本搜索、正则表达式引擎、数据压缩、字符串处理等领域,能够显著提高匹配效率。 +常规算法与KMP算法比较: +- 常规算法匹配字符串:从主串的起始位置(或指定位置)开始与模式串的第一个字符比较,若相等则继续逐个比较后续字符;否则从主串的下一个字符再重新和模式串的字符比较。依次类推,直到模式串成功匹配,返回主串中第一次出现模式串字符的位置,或者模式串匹配不成功,这里约定返回-1。 ![数据结构与算法-042](/iblog/posts/annex/images/essays/数据结构与算法-042.gif) - 从主串的起始位置(或指定位置)开始与模式串的第一个字符比较,若相等,则继续逐个比较后续字符;否则从主串的下一个字符再重新和模式串的字符比较。依次类推,直到模式串成功匹配,返回主串中第一次出现模式串字符的位置,或者模式串匹配不成功,这里约定返回-1。 +- KMP算法匹配字符串:主要是改进了暴力匹配中i回溯的操作,KMP算法中当一趟匹配过程中出现字符比较不等时,不直接回溯i,而是利用已经得到的“部分匹配”的结果将模式串向右移动(j-next[j-1])的距离。 + ![数据结构与算法-043](/iblog/posts/annex/images/essays/数据结构与算法-043.gif) + +```java +public class KMPDemo { + public static void main(String[] args) { + KMP kmp = new KMP(); + String str1 = "BBC ABCDAB ABCDABCDABDE"; + String str2 = "ABCDABD"; + int[] next = kmp.getMatchTab(str2); + System.out.println(Arrays.toString(next)); + System.out.println(kmp.kmpSearch(str1, str2, next)); + } +} -- KMP算法匹配字符串 +class KMP { + + // 获取KMP 部分匹配表 + public int[] getMatchTab(String dest) { + int[] result = new int[dest.length()]; + // 部分匹配表第一个值始终为0 + result[0] = 0; + for (int i = 1, j = 0; i < result.length; i++) { + // KMP 核心(特点,公式) + while (j > 0 && dest.charAt(i) != dest.charAt(j)) { + j = result[j - 1]; + } + if (dest.charAt(j) == dest.charAt(i)) { + j++; + } + result[i] = j; + } + return result; + } - ![数据结构与算法-043](/iblog/posts/annex/images/essays/数据结构与算法-043.gif) + /** + * KMP查找算法 + * + * @param str1 原字符串 + * @param str2 子字符串 + * @param next 部分匹配表 + * @return 匹配到字符串的第一个索引位置 + */ + public int kmpSearch(String str1, String str2, int[] next) { + for (int i = 0, j = 0; i < str1.length(); i++) { + while (j > 0 && str1.charAt(i) != str2.charAt(j)) { + j = next[j - 1]; + } + if (str1.charAt(i) == str2.charAt(j)) { + j++; + } + if (j == str2.length()) { + return i - j + 1; + } + } + return -1; + } - 主要就是改进了暴力匹配中i回溯的操作,KMP算法中当一趟匹配过程中出现字符比较不等时,不直接回溯i,而是利用已经得到的“部分匹配”的结果将模式串向右移动(j-next[j-1])的距离。 - - ```java - public class KMPDemo { - public static void main(String[] args) { - KMP kmp = new KMP(); - String str1 = "BBC ABCDAB ABCDABCDABDE"; - String str2 = "ABCDABD"; - int[] next = kmp.getMatchTab(str2); - System.out.println(Arrays.toString(next)); - System.out.println(kmp.kmpSearch(str1, str2, next)); - } - } - - class KMP { - - // 获取KMP 部分匹配表 - public int[] getMatchTab(String dest) { - int[] result = new int[dest.length()]; - // 部分匹配表第一个值始终为0 - result[0] = 0; - for (int i = 1, j = 0; i < result.length; i++) { - // KMP 核心(特点,公式) - while (j > 0 && dest.charAt(i) != dest.charAt(j)) { - j = result[j - 1]; - } - if (dest.charAt(j) == dest.charAt(i)) { - j++; - } - result[i] = j; - } - return result; - } - - /** - * KMP查找算法 - * - * @param str1 原字符串 - * @param str2 子字符串 - * @param next 部分匹配表 - * @return 匹配到字符串的第一个索引位置 - */ - public int kmpSearch(String str1, String str2, int[] next) { - for (int i = 0, j = 0; i < str1.length(); i++) { - while (j > 0 && str1.charAt(i) != str2.charAt(j)) { - j = next[j - 1]; - } - if (str1.charAt(i) == str2.charAt(j)) { - j++; - } - if (j == str2.length()) { - return i - j + 1; - } - } - return -1; - } - - } - ``` +} +``` ## 贪心算法 -贪心算法又称贪婪算法,是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。 +贪心算法是一种通过在每一步选择中都做出当前看起来最优的选择,期望通过局部最优解得到全局最优解的算法策略。贪心算法通常适用于求解最优化问题。在某些问题中,贪心算法能提供最优解,但在其他问题中,贪心算法可能仅给出近似解。与动态规划不同,贪心算法不考虑全局信息,而是逐步做出决策。 + +贪心算法的基本思想是在每一个决策点都选择当前状态下最优的选择。每做出一个决策后,就锁定了该决策,不能回溯。整个过程通过局部的最优选择构建出最终的解。在执行过程中,贪心算法没有回头的机会,因此每一步的决策必须根据问题的特性,确保做出的是局部最优的选择。 + +优点在于它通常具有较高的执行效率。由于贪心算法每一步选择最优解,并不需要回溯,因此它的时间复杂度通常较低。它的简单性和高效性使得它适用于一些特定的最优化问题,如活动选择问题、最小生成树等。 +缺点是并不总能给出全局最优解。在某些情况下,局部最优解并不能导致全局最优解,因此无法解决所有的最优化问题。为了确保贪心算法能正确地找到全局最优解,问题必须满足“贪心选择性质”和“最优子结构”这两个条件。 + +贪心算法适用于那些具有贪心选择性质和最优子结构的问题。这类问题的解可以通过局部最优解逐步组合成全局最优解。常见的应用场景包括活动选择问题、最小生成树(如Kruskal算法、Prim算法)、单源最短路径问题(如Dijkstra算法)等。在这些场景中,贪心算法能够高效地找到问题的最优解。 -举例,假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号。 +举例,假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。如何选择最少的广播台,让所有的地区都可以接收到信号。 | 广播台 |覆盖地区 | |-----|-----| @@ -1408,8 +1669,6 @@ class KnapsackProblem { | K4 | “上海”, “天津” | | K5 | “杭州”, “大连” | - - ```java public class GreedyAlgorithmDemo { public static void main(String[] args) { @@ -1495,19 +1754,20 @@ class GreedyAlgorithm { ``` ## 普里姆算法 -普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索**最小生成树**。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。 +普里姆算法是一种用于求解加权无向图的最小生成树问题的贪心算法。该算法从图的某个节点开始,逐步扩展生成树,选择与当前生成树相连的权值最小的边,直到所有的节点都被包含在内。普里姆算法的时间复杂度为 O(E log V),其中 E 是边的数量,V 是节点的数量。 -> 最小生成树:给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树。简称MST。 -求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法。 -> -> - 普里姆算法:O(n^2),适合稠密图(边多的图) -> - 克鲁斯卡尔算法:O,适合稀疏图(边少的图) +> 最小生成树:给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,简称MST。求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法。 +> - 普里姆算法:O(n^2),适合稠密图(边多的图) +> - 克鲁斯卡尔算法:O,适合稀疏图(边少的图) -普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图。 +普里姆算法的优点在于它能够高效地求解最小生成树问题,特别是对于稠密图(即边数较多的图)较为高效。它的核心思想简单且容易理解,能够逐步通过贪心选择构建最小生成树。在图的边较多时,相较于Kruskal算法,普里姆算法通常表现更好。 +缺点是对于稀疏图(即边数较少的图),其效率较低。在实现时,普里姆算法需要维护一个优先队列来选择最小边,增加了空间复杂度和代码复杂度。在某些实现中,如果不使用适当的数据结构,算法可能变得较慢。 ![数据结构与算法-044](/iblog/posts/annex/images/essays/数据结构与算法-044.gif) -``` +普里姆算法适用于求解加权无向图的最小生成树问题,特别是图的边较多(即稠密图)时,普里姆算法通常更高效。它广泛应用于网络设计、城市交通规划、资源分配等领域,在这些问题中,我们常常需要找到一个包含所有点且总权重最小的连通子图。 + +```java public class PrimAlgorithmDemo { public static void main(String[] args) { char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'}; @@ -1610,21 +1870,26 @@ class MGraph { ``` ## 克鲁斯卡尔算法 -克鲁斯卡尔算法是求连通网的最小生成树的另一种方法。基本思想是, 将所有边按照权值的大小进行升序排序,然后从小到大一一判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。 直到具有 n 个顶点的连通网筛选出来 n-1(n为顶点个数) 条边为止。 +克鲁斯卡尔算法是一种用于求解加权无向图的最小生成树问题的贪心算法。它通过排序所有边,逐步选择权值最小的边,将这些边加入生成树中,直到生成树包含所有节点为止。与普里姆算法不同,克鲁斯卡尔算法是基于边的处理,而不是基于节点的处理。 + +克鲁斯卡尔算法是求连通网的最小生成树的另一种方法。基本思想是,将所有边按照权值的大小进行升序排序,然后从小到大一一判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分,反之舍去。直到具有 n 个顶点的连通网筛选出来 n-1(n为顶点个数) 条边为止。 -> 判断是否构成回路: 当每次需要将一条边添加到最小生成树时,判断该边的两个顶点终点是否相同,相同就会构成回路。 -> -> 关于终点的说明:就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是与它连通的最大顶点。 就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是与它连通的最大顶点。 +> 判断是否构成回路:当每次需要将一条边添加到最小生成树时,判断该边的两个顶点终点是否相同,相同就会构成回路。终点就是将所有顶点按照从小到大的顺序排列好之后,某个顶点的终点就是与它连通的最大顶点。 > -> 举例 -> - 首先ABCDEFG这7个顶点,在顶点集合中是按照顺序存放的; -> - 第一次选择的是EF,毫无疑问这一条边的终点是F; -> - 第二次选择的CD的终点D; -> - 第三次选择的DE,终点是F,因为此时D和E相连,D又和F相连,所以D的终点是F。而且,因为C和D是相连的,D和E相连,E和F也是相连的,所以C的终点此时变成了F。也就是说,当选择了EF、CD、DE这三条边后,C、D、E的终点都是F。当然F的终点也是F,因为F还没和后面的哪个顶点连接。 -> - 本来接下来应该选择CE的,但是由于C和E的终点都是F,所以就会形成回路; +> 举例: +> 1. 首先ABCDEFG这7个顶点,在顶点集合中是按照顺序存放的; +> 2. 第一次选择的是EF,毫无疑问这一条边的终点是F; +> 3. 第二次选择的CD的终点D; +> 4. 第三次选择的DE,终点是F,因为此时D和E相连,D又和F相连,所以D的终点是F。而且,因为C和D是相连的,D和E相连,E和F也是相连的,所以C的终点此时变成了F。也就是说,当选择了EF、CD、DE这三条边后,C、D、E的终点都是F。当然F的终点也是F,因为F还没和后面的哪个顶点连接。 +> 5. 本来接下来应该选择CE的,但是由于C和E的终点都是F,所以就会形成回路; ![数据结构与算法-045](/iblog/posts/annex/images/essays/数据结构与算法-045.gif) +克鲁斯卡尔算法的优点在于它实现简单,直观,并且适用于稀疏图(即边较少的图)。由于算法基于边的处理,只要对所有边进行排序并逐一检查,它的时间复杂度与边数的平方关系较弱,适合处理边数较少的图。使用并查集优化后,算法的效率得到了显著提高。 +缺点是它需要对所有的边进行排序,这在边数较多的情况下可能会导致效率较低。排序的时间复杂度为 O(E log E),其中 E 是边的数量。使用并查集来判断环的形成也需要额外的空间和时间。在图的边数非常多时,克鲁斯卡尔算法的效率可能不如普里姆算法。 + +克鲁斯卡尔算法特别适合用于边数较少的稀疏图,或者当图的边的数量远远大于节点的数量时。由于它是基于边的贪心选择来构建最小生成树,因此在很多网络设计、资源连接等问题中应用广泛,如计算机网络的最小拓扑、道路建设等领域。 + ```java public class KruskalCaseDemo { @@ -1819,12 +2084,15 @@ class EdgeData { ``` ## 迪杰斯特拉算法 -迪杰斯特拉算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。迪杰斯特拉算法是基于贪心思想,从起始位置触发,每次寻找与起点位置距离且未访问过的顶点,以该顶点作为中间结点,更新从起点到其他顶点的距离,直到全部顶点都作为了中间结点,并完成了路径更新,算法结束。 它的主要特点是以起始点为中心向外层层扩展,即图的广度优先搜索思想,直到扩展到终点为止。 - -视频讲解:[bilibili](https://www.bilibili.com/video/BV1uX4y137Hf) +迪杰斯特拉算法是一种用于求解单源最短路径问题的贪心算法。它的目标是从图中的某个起点出发,找到到达图中所有其他节点的最短路径。该算法适用于权值非负的加权图,可以有效地计算从一个节点到其他所有节点的最短路径。 ![数据结构与算法-046](/iblog/posts/annex/images/essays/数据结构与算法-046.gif) +迪杰斯特拉算法的优点在于它的实现简单,且在权值非负的图中非常高效。它能够在一次遍历中找到从源节点到所有其他节点的最短路径,适用于很多实际应用场景,如网络路由、地图导航等。 +主要缺点是它不能处理负权边的问题。如果图中包含负权边,算法的结果将不正确。另一个缺点是,当图的规模非常大时,算法的时间复杂度可能较高,尤其是在没有使用合适数据结构(如优先队列)时,时间复杂度为 O(V²),其中 V 是图中的节点数。 + +适用于寻找加权图中单源的最短路径,特别是在图的边的权值非负时。在很多实际问题中,如通信网络、交通路线规划等,常常需要计算从某个源节点到其他所有节点的最短路径。该算法被广泛应用于计算机网络路由、地图导航、资源分配等领域。 + ```java public class DijkstraAlgorithmDemo { public static void main(String[] args) { @@ -2003,13 +2271,12 @@ class VisitedVertex { ``` ## 弗洛伊德算法 -弗洛伊德算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与迪杰斯特拉算法类似。 +弗洛伊德算法是一种用于求解加权图中所有节点对之间最短路径的算法,也称为“所有最短路径”算法。与迪杰斯特拉算法仅解决单源最短路径不同,弗洛伊德算法能够同时计算图中每一对节点之间的最短路径。它是基于动态规划的思想,适用于任意类型的图,包括带负权边的图但不适用于负权环。 -> 迪杰斯特拉算法对比弗洛伊德算法: -> 迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。 -> - 弗洛伊德算法计算图中各个顶点之间的最短路径 -> - 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径 +弗洛伊德算法的优点是能够计算图中所有节点对之间的最短路径,且对图的规模不受限制。算法非常简洁,适合处理较小规模的图。它能够处理带负权边的图,但不能处理负权环。因为它考虑的是所有的节点对,因此特别适合需要全局最短路径信息的应用场景。 +缺点是时间复杂度较高,其时间复杂度为 O(n³),其中 n 是图中节点的数量。这使得弗洛伊德算法在处理大规模图时效率较低。弗洛伊德算法需要 O(n²) 的空间来存储最短路径信息,这对于节点数非常大的图来说可能占用较多内存。 +弗洛伊德算法适用于需要计算图中每一对节点最短路径的场景,尤其是当图的节点数量较小或中等时。在某些网络、地图、路径规划等应用中,了解每一对节点之间的最短路径是很重要的。它可以用于最短路径查询、图的全局优化、传输网络的路径分析等场景。对于小规模的图或需要全局最短路径的情况,弗洛伊德算法是一个合适的选择。 ```java public class FloydAlgorithmDemo { @@ -2079,135 +2346,3 @@ class FloydGraph { } ``` -## 马踏棋盘算法 -马踏棋盘算法也被称为骑士周游问题。将马随机放在国际象棋的8×8棋盘0~7的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。 - -马踏棋盘问题实际上是图的深度优先搜索(DFS)的应用。 - -```java -public class HouseChessBoardDemo { - public static void main(String[] args) { - HouseChessBoard houseChessBoard = new HouseChessBoard(7, 7, 2, 4); - houseChessBoard.showChessBoard(); - } -} - -class HouseChessBoard { - - /** - * 表示棋盘的列 - */ - private int x; - /** - * 表示棋盘的行 - */ - private int y; - /** - * 创建一个数组,标记棋盘的各个位置是否被访问过,true表示已经访问过 - */ - private boolean visited[]; - /** - * 使用一个属性,标记是否棋盘的所有位置都被访问 如果为true,表示成功 - */ - private boolean finished; - - private int[][] chessboard; - - public HouseChessBoard(int x, int y, int row, int column) { - this.x = x; - this.y = y; - this.visited = new boolean[x * y]; - this.chessboard = new int[x][y]; - traversalChess(this.chessboard, row-1, column-1, 1); - } - - public void showChessBoard(){ - for (int[] ints : this.chessboard) { - for (int anInt : ints) { - System.out.print(anInt + "\t"); - } - System.out.println(); - } - } - - public void traversalChess(int[][] chessboard, int row, int column, int step) { - chessboard[row][column] = step; - - // 将当前位置标记为已经访问过 - visited[row * x + column] = true; - - // 获取当前位置的下一个可走通的位置的集合 - ArrayList nextPos = getNext(new Point(column, row)); - - sort(nextPos); - - while (nextPos.size() > 0) { - // 获取当前可走通的位置 - Point current = nextPos.remove(0); - // 判断当前该点是否被访问过,如果没有被访问过则继续向下访问 - if (!visited[current.y * x + current.x]) { - traversalChess(chessboard, current.y, current.x, step + 1); - } - } - - // 当遍历完可走的位置集合后,如果发现该路不通,则进行回溯,否则标记为完成 - if (step < x * y && !finished) { - chessboard[row][column] = 0; - visited[row * x + column] = false; - } else { - finished = true; - } - } - - // 将可走通路根据回溯次数进行从小到大排序 - public void sort(ArrayList points){ - points.sort((o1, o2) -> { - int next1 = getNext(o1).size(); - int next2 = getNext(o2).size(); - return next1 - next2; - }); - } - - - - // 获取下一个可走的位置 - public ArrayList getNext(Point current) { - ArrayList ps = new ArrayList(); - Point p1 = new Point(); - // 表示马儿可以走5这个位置 - if ((p1.x = current.x - 2) >= 0 && (p1.y = current.y - 1) >= 0) { - ps.add(new Point(p1)); - } - // 判断马儿可以走6这个位置 - if ((p1.x = current.x - 1) >= 0 && (p1.y = current.y - 2) >= 0) { - ps.add(new Point(p1)); - } - // 判断马儿可以走7这个位置 - if ((p1.x = current.x + 1) < x && (p1.y = current.y - 2) >= 0) { - ps.add(new Point(p1)); - } - // 判断马儿可以走0这个位置 - if ((p1.x = current.x + 2) < x && (p1.y = current.y - 1) >= 0) { - ps.add(new Point(p1)); - } - // 判断马儿可以走1这个位置 - if ((p1.x = current.x + 2) < x && (p1.y = current.y + 1) < y) { - ps.add(new Point(p1)); - } - // 判断马儿可以走2这个位置 - if ((p1.x = current.x + 1) < x && (p1.y = current.y + 2) < y) { - ps.add(new Point(p1)); - } - // 判断马儿可以走3这个位置 - if ((p1.x = current.x - 1) >= 0 && (p1.y = current.y + 2) < y) { - ps.add(new Point(p1)); - } - // 判断马儿可以走4这个位置 - if ((p1.x = current.x - 2) >= 0 && (p1.y = current.y + 1) < y) { - ps.add(new Point(p1)); - } - return ps; - } - -} -``` diff --git a/blog-site/public/posts/java/javaessay/java-algorithms/index.html b/blog-site/public/posts/java/javaessay/java-algorithms/index.html index 9256de2f..43273c0a 100644 --- a/blog-site/public/posts/java/javaessay/java-algorithms/index.html +++ b/blog-site/public/posts/java/javaessay/java-algorithms/index.html @@ -226,6 +226,34 @@ + + + + + + + + + +