github地址:https://github.com/feddiyao/Frontend-05-Template/tree/master/Week%2002
地图编辑器
首先实现一个地图编辑器,随着鼠标在页面上的点击滑动可以进行地图的绘画,并给出了一个save按钮来进行编辑后的地图内容的保存
<style>
.cell {
display: inline-block;
line-height: 7px;
width: 6px;
height: 6px;
background-color: gray;
border-bottom: solid 1px white;
border-right: solid 1px white;
vertical-align: middle;
vertical-align: top;
}
#container {
width: 701px;
}
</style>
<div id="container">
</div>
<button onclick="localStorage['map'] = JSON.stringify(map)">save</button>
<script>
let map = localStorage["map"] ? JSON.parse(localStorage["map"]) : Array(10000).fill(0);
let container = document.getElementById("container");
for(let y = 0; y < 100; y++) {
for(let x = 0; x < 100; x ++) {
let cell = document.createElement("div");
cell.classList.add("cell");
if(map[100*y + x] == 1)
cell.style.backgroundColor = "black";
//按住的时候移动会进行画图
cell.addEventListener("mousemove", () => {
if(mousedown) {
if(clear) {
cell.style.backgroundColor = "";
map[100 * y + x] = 0;
} else {
cell.style.backgroundColor = "black";
map[100*y + x] = 1;
}
}
})
container.appendChild(cell);
}
}
let mousedown = false;
let clear = false;
//是否是按下事件
document.addEventListener("mousedown", e => {
mousedown = true;
//判断按下时是否在按右键
clear = (e.which === 3)
});
document.addEventListener("mouseup", () => mousedown = false);
//将默认的右键弹出菜单事件禁止掉
document.addEventListener("contextmenu", e => e.preventDefault());
function path(map, start, end) {
}
</script>
广度优先搜索
寻路问题:从某个点能走到哪些点就是不断地把它周边的点不停地加进集合的过程,乍一看有点像递归,但是我们要知道,递归就相当于深度优先遍历,对于寻路问题来说,递归是不好的,广度优先搜索才是好的。
完成了地图的绘制,现在我们来考虑实现广度优先搜索的功能。
首先进行基础知识的补全:
javascript中的pop push shift unshift
push:在数组的尾部加入一个元素,并返回原有length+1的长度。
unshift:在数组的头部加入一个元素,并返回原有length+1的长度。
pop:删除数组尾部第一个元素,并返回这个元素。
shift:删除数组头部的第一个元素,并返回这个元素。
所以我们可以得出这样的结论
push + shift 可以实现对队列的操作
pop + unshift 可以实现队列操作
push + pop 可以实现栈的操作
shif + unshift 可以实现栈的操作(不常用 性能会变低)
看一下我们最终的path算法实现:
function path(map, start, end) {
//所有搜索算法差异的灵魂queue
var queue = [start]
function insert(x, y) {
if (x < 0 || x >= 100 || y < 0 || y >= 100)
return;
if (map[y * 100 + x])
return;
map[y * 100 + x] = 2;
queue.push([x, y])
}
while(queue.length){
let [x, y] = queue.shift();
console.log(x, y);
if (x === end[0] && y === end[1]) {
return true;
}
insert(x - 1, y);
insert(x, y - 1);
insert(x + 1, y);
insert(x, y + 1);
}
}
看一下算法的运行结果:
值得一提的是,在这里,如果把push和shift方法修改为push和pop方法,那么实现的是一个深度优先搜索,这里可以自己体会。
可视化寻路算法
我们可以用async函数实现可视化的寻路算法:
function sleep(t) {
return new Promise(function(resolve) {
setTimeout(resolve, t);
});
}
async function path(map, start, end) {
//所有搜索算法差异的灵魂queue
var queue = [start]
async function insert(x, y) {
if (x < 0 || x >= 100 || y < 0 || y >= 100)
return;
if (map[y * 100 + x])
return;
await sleep(30);
container.children[y * 100 + x].style.backgroundColor = "lightgreen";
map[y * 100 + x] = 2;
queue.push([x, y])
}
while(queue.length){
let [x, y] = queue.shift();
console.log(x, y);
if (x === end[0] && y === end[1]) {
return true;
}
await insert(x - 1, y);
await insert(x, y - 1);
await insert(x + 1, y);
await insert(x, y + 1);
}
}
路径问题
我们要如何处理最终找寻的路径呢?只要我们在map中做标记的时候不标记为2,而标记当前节点的前驱节点便好了。
修改后的代码:
function sleep(t) {
return new Promise(function(resolve) {
setTimeout(resolve, t);
});
}
async function findPath(map, start, end) {
let table = Object.create(map)
//所有搜索算法差异的灵魂queue
var queue = [start]
async function insert(x, y, pre) {
if (x < 0 || x >= 100 || y < 0 || y >= 100)
return;
if (map[y * 100 + x])
return;
await sleep(1);
container.children[y * 100 + x].style.backgroundColor = "lightgreen";
map[y * 100 + x] = pre;
queue.push([x, y])
}
while(queue.length){
let [x, y] = queue.shift();
console.log(x, y);
if (x === end[0] && y === end[1]) {
let path = [];
while(x != start[0] || y != start[1]) {
path.push(table[y * 100 + x]);
[x, y] = table[y * 100 + x];
await sleep(30);
container.children[y * 100 + x].style.backgroundColor = "purple";
}
return path
}
await insert(x - 1, y, [x, y]);
await insert(x, y - 1, [x, y]);
await insert(x + 1, y, [x, y]);
await insert(x, y + 1, [x, y]);
}
return null;
}
看下运行的效果图:
启发式搜索
即按照特定的优先级进行寻路,在一定的函数支持下,能够实现这样的寻路毕竟是包含最佳路径的。
那么我们需要更改的代码是什么呢?我们只需要把一个queue改成一个以一定的优先级来提供点的数据结构就可以了。
看一下添加的sorted函数:
class Sorted {
constructor(data, compare) {
this.data = data.slice();
this.compare = compare || ((a, b) => a - b);
}
//每次take的时候拿出来的一定是最小的
take() {
if (!this.data.length)
return;
let min = this.data[0];
let minIndex = 0;
for(let i = 1; i < this.data.length; i++) {
if(this.compare(this.data[i], min) < 0) {
min = this.data[i];
minIndex = i;
}
}
//slice的删除会把剩下的元素往前挪,那么时间复杂度则为o(n) 这不是我们想要见到的
//这里的做法是把当前元素和最后一个元素交换,再进行一次pop操作
this.data[minIndex] = this.data[this.data.length - 1];
this.data.pop();
return min
}
give(v) {
this.data.push(v);
}
}
运行结果图:
此时就可以在findPath中加入这些逻辑了,看下最终的代码:
<style>
.cell {
display: inline-block;
line-height: 7px;
width: 6px;
height: 6px;
background-color: gray;
border-bottom: solid 1px white;
border-right: solid 1px white;
vertical-align: middle;
vertical-align: top;
}
#container {
width: 701px;
}
</style>
<div id="container"></div>
<button onclick="localStorage['map'] = JSON.stringify(map)">save</button>
<script>
class Sorted {
constructor(data, compare) {
this.data = data.slice();
this.compare = compare || ((a, b) => a - b);
}
//每次take的时候拿出来的一定是最小的
take() {
if (!this.data.length)
return;
let min = this.data[0];
let minIndex = 0;
for(let i = 1; i < this.data.length; i++) {
if(this.compare(this.data[i], min) < 0) {
min = this.data[i];
minIndex = i;
}
}
//slice的删除会把剩下的元素往前挪,那么时间复杂度则为o(n) 这不是我们想要见到的
//这里的做法是把当前元素和最后一个元素交换,再进行一次pop操作
this.data[minIndex] = this.data[this.data.length - 1];
this.data.pop();
return min
}
give(v) {
this.data.push(v);
}
}
let map = localStorage["map"] ? JSON.parse(localStorage["map"]) : Array(10000).fill(0);
let container = document.getElementById("container");
for(let y = 0; y < 100; y++) {
for(let x = 0; x < 100; x ++) {
let cell = document.createElement("div");
cell.classList.add("cell");
if(map[100*y + x] == 1)
cell.style.backgroundColor = "black";
//按住的时候移动会进行画图
cell.addEventListener("mousemove", () => {
if(mousedown) {
if(clear) {
cell.style.backgroundColor = "";
map[100 * y + x] = 0;
} else {
cell.style.backgroundColor = "black";
map[100*y + x] = 1;
}
}
})
container.appendChild(cell);
}
}
let mousedown = false;
let clear = false;
//是否是按下事件
document.addEventListener("mousedown", e => {
mousedown = true;
//判断按下时是否在按右键
clear = (e.which === 3)
});
document.addEventListener("mouseup", () => mousedown = false);
//将默认的右键弹出菜单事件禁止掉
document.addEventListener("contextmenu", e => e.preventDefault());
function sleep(t) {
return new Promise(function(resolve) {
setTimeout(resolve, t);
});
}
async function findPath(map, start, end) {
let table = Object.create(map)
//所有搜索算法差异的灵魂queue
var queue = new Sorted([start], (a, b) => distance(a) - distance(b))
async function insert(x, y, pre) {
if (x < 0 || x >= 100 || y < 0 || y >= 100)
return;
if (map[y * 100 + x])
return;
await sleep(1);
container.children[y * 100 + x].style.backgroundColor = "lightgreen";
map[y * 100 + x] = pre;
queue.data.push([x, y])
}
function distance(point) {
return (point[0] - end[0]) ** 2 + (point[1] - end[1]) ** 2;
}
while(queue.data.length){
let [x, y] = queue.take();
console.log(x, y);
if (x === end[0] && y === end[1]) {
let path = [];
while(x != start[0] || y != start[1]) {
path.push(table[y * 100 + x]);
[x, y] = table[y * 100 + x];
await sleep(30);
container.children[y * 100 + x].style.backgroundColor = "purple";
}
return path
}
await insert(x - 1, y, [x, y]);
await insert(x, y - 1, [x, y]);
await insert(x + 1, y, [x, y]);
await insert(x, y + 1, [x, y]);
// await insert(x - 1, y - 1, [x, y]);
// await insert(x + 1, y - 1, [x, y]);
// await insert(x + 1, y + 1, [x, y]);
// await insert(x - 1, y + 1, [x, y]);
}
return null;
}
</script>
运行效果图: