上一章我们学习了求第K大元素的两种方法比较。
【一起学数据结构与算法分析】第一篇:取第K大数的两种方法比较
今天我们紧接着学习引论中的第二个问题:字谜游戏
首先我们分别定义字谜板(上图中的表格)和字典,字典暂且用一个简单的列表来模拟。
companion object {
/**
* List as English word dictionary.
*/
private val DICTIONARY = arrayOf("this", "two", "fat", "that")
/**
* 2d array named puzzle board.
*/
private val PUZZLE_BOARD = arrayOf(
arrayOf("t", "h", "i", "s"),
arrayOf("w", "a", "t", "s"),
arrayOf("o", "a", "h", "g"),
arrayOf("f", "g", "d", "t")
)
}
有兴趣的同学可以补充一个完整的字典文件或者调用公共的api,不过这些都是细节,无碍我们写算法。
按照书中的提示:
检查每一个有序三元组(行、列、方向)验证是否有单词存在.
行、列倒好理解,可这个方向我们需要琢磨下,经过老朽慎密的思考,给出了如下定义:
/**
* All directions for finding out.
* @param xStep is the offset when moving on x-axis.
* @param yStep is the offset when moving on y-axis.
*/
enum class Direction(val xStep: Int, val yStep: Int) {
Left(-1, 0),
Top(0, -1),
Right(1, 0),
Bottom(0, 1),
TopLeft(-1, -1),
TopRight(-1, 1),
BottomLeft(1, -1),
BottomRight(1, 1)
}
就是说,首先共有8个方向,每个方向还带有两个步长,就拿第一个Left来说吧,表示左方向移动一次在【列】上移动-1个单位,在【行】上移动0个单位。
为了加深理解,老朽还是简单地补一张图吧。。。
方向定义好了,接下来就是循环了:先遍历字谜表的行,再遍历列,再根据方向遍历,再遍历取出来的字符串以判断是否在字典里……
听到这些遍历什么感受????
不说了,直接上代码。
/**
* Method1: For every direction of every letter inside of [PUZZLE_BOARD], detect whether the directed line contains
* invalid word in [DICTIONARY].If word recognized, print its location with rows, cols, direction and letter count.
*/
@Test
fun detectWords() {
// Loop puzzle board by rows.
PUZZLE_BOARD.forEachIndexed { rows, rowsItems ->
// Loop the puzzle board by cols.
rowsItems.forEachIndexed { cols, _ ->
// Loop by directions.
Direction.values().forEachIndexed { _, direction ->
// Retrieve all string by rows, cols, and directions.
val suspiciousWords = getSuspiciousWords(PUZZLE_BOARD, rows, cols, direction)
// Loop the retrieved list
suspiciousWords?.forEachIndexed { _, s ->
// If the string is a valid word, print word and its positions.
if(isValidWord(s)) {
print(s, rows, cols, direction)
}
}
}
}
}
}
其中方法getSuspiciousWords就是根据起点和方向找到这个方向上的所有字符串。
/**
* For every direction of every letter inside of [PUZZLE_BOARD], retrieve all string on the the directed line with
* start point fixed.
* @param rows is rows of start point.
* @param cols is cols of start point.
* @param direction is the loop direction to find out words.
* @return list contains suspicious words, null or empty list if no string found out.
*/
private fun getSuspiciousWords(array: Array<Array<String>>, rows: Int, cols: Int, direction: Direction): List<String>? {
// return null if rows and cols exceed the range of array.
if(array.isEmpty()) {
return null
}
val arrayRows = array.size
val arrayCols = array[0].size
if(rows < 0 || rows >= arrayRows || cols < 0 || cols >= arrayCols) {
return null
}
val resultList = ArrayList<String>()
// To the assigned direction, add the positions string to the result list.
var currentRows = rows
var currentCols = cols
// Add the first letter as well as the start point.
val sb = StringBuilder(array[currentRows][currentCols])
resultList.add(sb.toString())
while (true) {
currentRows += direction.yStep
currentCols += direction.xStep
// Check range!
if(currentRows < 0 || currentRows >= arrayRows || currentCols < 0 || currentCols >= arrayCols) {
break
} else {
resultList.add(sb.append(array[currentRows][currentCols]).toString())
}
}
return resultList
}
根据这个粗暴的算法,我们也顺利找到了字谜。
好了,主要代码就这么多,思路其实很简单,就是效率非常低下,不信你弄个64*64的字谜板并且用真实字典试试,估计够吃一把鸡了。
接下来我们分析下该算法的时间复杂度。
总时间复杂度为:O(n) * O(n) * 8 * (O(n) + O(n)) = O(n^3)。
其实就算使用如些粗暴的方法,我们仍可以适当优化,如按书中提示指定最大单词长度,如30,反正老朽的六级水平还没有看过30个字母以上的字词哈~
当然了,以上算法只是最直观的实现,还是那句话,慢慢把这本书看下去。其实学算法最有趣的事情,莫过于看到了更优解,而不是最优解。
最后再奉上代码地址:https://github.com/codersth/dsa-study.git
文件:WordRiddleTest.kt