最近又再看《剑指Offer数据结构与算法》了,看到里面的回溯算法时想起我当时在某DN发过一个Java版的点菜小Demo,这次把他用纯 html/css/js 实现以下,可以使用浏览器在线使用,目前是个小Demo,所以暂且具备下面功能:
- 菜品的添加、删除和修改
- 价格区间的设定
- 菜品数量的设定
- 生成符合给定条件的策略
- 对生成的结果自动排序
可通过点击:Demo演示地址 查看演示。
暂且具备上面的内容,后期有空了再完善。回溯算法的原理是每次在进行下一步的时候会有同样的两种决策,例如要求数组 [1,2] 的所有子集,从空数组开始,每次会面临两个选择,将元素添加到空数组中,和不添加进去,具体的原理可以查询其他大佬的文章,下面是使用JavaScript实现的回溯算法方法:
function makeOrder(foodMenuList, index, maxPriceSum, minPriceSum, size, helperList, resultList){
var priceList = helperList.map(item=>item.foodPrice);
var priceSum = sum(priceList);
if(priceSum >= minPriceSum && priceSum <= maxPriceSum && index == foodMenuList.length && helperList.length > 0){
if(size == -1){
resultList.push(deepClone(helperList));
}else{
if(size == helperList.length){
resultList.push(deepClone(helperList));
}
}
}else if(index < foodMenuList.length){
makeOrder(foodMenuList, index+1, maxPriceSum, minPriceSum, size, helperList, resultList);
helperList.push(foodMenuList[index]);
makeOrder(foodMenuList, index+1, maxPriceSum, minPriceSum, size, helperList, resultList);
helperList.splice(helperList.length - 1, 1);
}
}
参数解释:
- foodMenuList: 菜单列表
- index: 索引
- maxPriceSum: 最高菜品价格总和
- minPriceSum: 最低菜品价格总和
- size: 菜品数量
- helperList: 辅助数组
- resultList: 最终结果集
最终实现效果如下:
完整代码(复制可直接运行):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta describe="这是一个通过回溯算法实现的点菜Demo,可以根据价格限制和菜的数量限制快速决策出可以点的策略。">
<title>点菜助手Demo</title>
<style>
html,body{
margin: 0;
height: 100vh;
}
header{
font-size: 18px;
font-family: '宋体';
font-weight: bold;
color: white;
background-color: black;
padding: 10px;
}
main{
height: calc(100vh - 45px);
display: flex;
flex-direction: column;
background-color: antiquewhite;
}
table{
margin: 10px;
font-size: 14px;
border: 1px solid black;
border-spacing: 0;
width: calc(100% - 20px);
overflow-y: auto;
}
th,td{
border: 1px solid black;
padding: 5px 10px;
}
thead{
top: 10px;
color: black;
}
td:first-child{
width: 60px;
text-align: center;
}
td:last-child{
width: 400px;
text-align: center;
}
td:last-child > button{
margin-right: 10px;
}
.toolBar{
margin: 10px 10px 0 10px;
display: flex;
align-items: center;
}
button{
width: 100px;
border: none;
height: 30px;
border-radius: 4px;
margin-right: 10px;
cursor: pointer;
}
.tableBox{
max-height: 400px;
overflow: auto;
}
input{
height: 20px;
}
.tool-item{
margin-right: 20px;
font-size: 14px;
}
button:hover{
opacity: 0.9;
}
.btn-info {
color: white;
background-color: orange;
}
.btn-del {
color: white;
background-color: red;
}
.btn-edit {
color: white;
background-color: gray;
}
.line{
margin-top: 10px;
border-top: 1px solid black;
}
.resultBox{
max-height: calc(100vh - 495px);
overflow: auto;
}
.result{
font-size: 14px;
}
#editDialog{
width: 500px;
position: absolute;
margin-top: 60px;
border: 1px solid black;
background-color: black;
color: white;
outline: none;
border-radius: 4px;
box-shadow: 3px 6px 10px black;
}
.dialog-title{
font-weight: bold;
}
.dialog-body{
padding: 20px 0;
}
.form-item{
margin-bottom: 20px;
display: flex;
align-items: center;
}
label{
font-weight: bold;
}
input{
padding: 2px 10px;
border-radius: 4px;
}
</style>
</head>
<body>
<header>点菜助手</header>
<main>
<div class="toolBar">
<div class="tool-item">
<label for="minPrice">预算</label>
<input style="width: 100px; text-align: center;" value="0" id="minPrice" type="number" placeholder="最低"><span> - </span>
<input style="width: 100px; text-align: center;" id="maxPrice" type="number" placeholder="最高">
</div>
<div class="tool-item">
<label for="foodSize">菜品数量</label>
<input id="foodSize" type="number" placeholder="菜品数量(可选)">
</div>
<button class="btn-info" id="startMake">生成点菜计划</button>
<button class="btn-info" id="addFoodMenu">添加菜品</button>
</div>
<div class="tableBox">
<table>
<thead>
<tr>
<th>序号</th>
<th>菜名</th>
<th>价格(元)</th>
<th>操作</th>
</tr>
</thead>
<tbody id="menuTable"></tbody>
</table>
</div>
<div class="line"></div>
<div class="resultBox">
<div style="padding: 10px 0 0 10px; font-size: 14px; font-weight: bold;">生成总数:<span id="resultTotal">0</span></div>
<ul id="result"></ul>
</div>
<dialog id="editDialog">
<div class="dialog-title">菜品信息</div>
<div class="dialog-body">
<form>
<div class="form-item">
<label style="width: 60px;" for="foodName">菜名</label>
<input placeholder="菜名" type="text" id="foodName" style="width: 100%;">
</div>
<div class="form-item">
<label style="width: 60px;" for="foodPrice">价格</label>
<input placeholder="价格" type="Number" id="foodPrice" style="width: 100%;">
</div>
</form>
<div class="form-item" style="justify-content: end;">
<button class="btn-info" style="width: 100px; margin-right: 10px;" id="closeDialog">取消</button>
<button class="btn-info" style="width: 100px;" id="submitForm">提交</button>
</div>
</div>
</dialog>
</main>
<script>
var isAdd = true; //标记dialog提交按钮是新增还是修改
var currentEditFoodId = -1; //当前编辑的foodId
//默认菜单
var defaultFoodMenuList = [
{id:0, foodName: '米饭', foodPrice: 4},
{id:1, foodName: '鱼香肉丝', foodPrice: 48},
{id:2, foodName: '糖醋排骨', foodPrice: 68},
{id:3, foodName: '京酱肉丝', foodPrice: 48},
{id:4, foodName: '地锅鸡', foodPrice: 78},
{id:5, foodName: '蚂蚁上树', foodPrice: 178},
{id:6, foodName: '毛氏红烧肉', foodPrice: 79},
{id:7, foodName: '蒜末拍黄瓜', foodPrice: 12}
];
function init(){
var localFoodList = localStorage.getItem('foodList');
if(localFoodList){
defaultFoodMenuList = JSON.parse(localFoodList)
}
}
init()
function saveFoodListToLocal(foodList){
localStorage.setItem('foodList', JSON.stringify(foodList));
}
//获取指定ID的Element
function getElById(elId){
return document.getElementById(elId);
}
//显示Dialog
getElById('addFoodMenu').addEventListener('click',()=>{
isAdd = true;
getElById('foodName').value = '';
getElById('foodPrice').value = '';
getElById('editDialog').hidden = false;
getElById('editDialog').show();
})
//关闭Dialog
getElById('closeDialog').addEventListener('click',()=>{
getElById('editDialog').hidden = true;
})
//提交新增或修改
getElById('submitForm').addEventListener('click',()=>{
var foodName = getElById('foodName').value;
var foodPrice = Number(getElById('foodPrice').value);
if(!foodName || !foodPrice){
alert('请输入菜名和价格后提交')
return;
}else{
if(isAdd){
if(defaultFoodMenuList.filter(item=>item.foodName == foodName).length > 0){
alert(foodName+'已经存在,如需修改请点击编辑按钮进行修改');
return;
}
defaultFoodMenuList.push({id: defaultFoodMenuList[defaultFoodMenuList.length-1].id+1, foodName: foodName, foodPrice: foodPrice});
}else{
console.log("进入修改")
defaultFoodMenuList.forEach(element => {
if(element.id == currentEditFoodId){
element.foodName = foodName;
element.foodPrice = foodPrice;
}
});
}
saveFoodListToLocal(defaultFoodMenuList);
buildMenuTable(defaultFoodMenuList)
getElById('closeDialog').click();
}
})
//构建表格渲染
function buildMenuTable(foodMenuList){
var menuTableEl = getElById("menuTable");
cleanAndrender(menuTableEl);
foodMenuList.forEach(foodMenu => {
var trNode = document.createElement("tr");
var idNode = document.createElement("td");
idNode.append(document.createTextNode(foodMenu.id));
var foodNameNode = document.createElement("td");
foodNameNode.append(document.createTextNode(foodMenu.foodName));
var foodPriceNode = document.createElement("td");
foodPriceNode.append(document.createTextNode(foodMenu.foodPrice));
var editNode = document.createElement("td");
var editBtn = document.createElement("button");
editBtn.classList.add('btn-edit')
var delBtn = document.createElement("button");
delBtn.classList.add('btn-del')
delBtn.addEventListener('click',()=>{deleteFood(foodMenu)})
editBtn.addEventListener('click',()=>{editFood(foodMenu)})
editBtn.appendChild(document.createTextNode("编辑"))
delBtn.appendChild(document.createTextNode("删除"))
editNode.append(editBtn);
editNode.append(delBtn);
trNode.appendChild(idNode);
trNode.appendChild(foodNameNode);
trNode.appendChild(foodPriceNode);
trNode.appendChild(editNode)
menuTableEl.appendChild(trNode);
});
}
//删除菜单
function deleteFood(item){
defaultFoodMenuList.forEach((element,index) => {
if(element.id == item.id){
defaultFoodMenuList.splice(index,1);
}
});
buildMenuTable(defaultFoodMenuList);
}
//编辑菜单
function editFood(item){
isAdd = false;
currentEditFoodId = item.id;
getElById('foodName').value = item.foodName;
getElById('foodPrice').value = item.foodPrice;
getElById('editDialog').hidden = false;
getElById('editDialog').show();
}
//初始化表格
buildMenuTable(defaultFoodMenuList);
//生成策略
function makeOrder(foodMenuList, index, maxPriceSum, minPriceSum, size, helperList, resultList){
var priceList = helperList.map(item=>item.foodPrice);
var priceSum = sum(priceList);
if(priceSum >= minPriceSum && priceSum <= maxPriceSum && index == foodMenuList.length && helperList.length > 0){
if(size == -1){
resultList.push(deepClone(helperList));
}else{
if(size == helperList.length){
resultList.push(deepClone(helperList));
}
}
}else if(index < foodMenuList.length){
makeOrder(foodMenuList, index+1, maxPriceSum, minPriceSum, size, helperList, resultList);
helperList.push(foodMenuList[index]);
makeOrder(foodMenuList, index+1, maxPriceSum, minPriceSum, size, helperList, resultList);
helperList.splice(helperList.length - 1, 1);
}
}
//对菜价求和
function sum(priceList) {
var result = 0;
priceList.forEach(element => {
result += element;
});
return result;
}
//拼接菜名
function joinName(nameList) {
var result = "";
for(let i=0; i<nameList.length; i++){
if(i == nameList.length - 1){
result = result + nameList[i];
}else{
result = result +nameList[i] + ", ";
}
}
return result;
}
//深拷贝
function deepClone(list){
var jsonList = JSON.stringify(list);
return JSON.parse(jsonList);
}
//开始生成按钮事件
getElById('startMake').addEventListener('click',()=>{
var resultList = []
var helperList = []
var maxPrice = getElById('maxPrice').value;
var minPrice = getElById('minPrice').value;
var foodSize = getElById('foodSize').value;
if(!maxPrice){
alert('请输入你的最大预算')
return;
}
foodSize = foodSize ? foodSize : -1;
console.log("预算:"+maxPrice+"; 指定数量:"+foodSize)
makeOrder(defaultFoodMenuList, 0, maxPrice,minPrice,foodSize, helperList, resultList);
var resultEl = getElById('result');
cleanAndrender(resultEl)
if(resultList.length == 0){
alert("没有满足该条件的策略")
return
}
getElById('resultTotal').innerText = resultList.length;
quickSort(resultList,0,resultList.length-1)
resultList.forEach(element => {
var foodNameListStr = ""
var foodNameList = element.map(i=>i.foodName);
var priceList = element.map(i=>i.foodPrice);
var priceSum = sum(priceList);
foodNameListStr = joinName(foodNameList);
var liNode = document.createElement('li');
liNode.append(document.createTextNode("菜品: "+foodNameListStr+"; 总价: "+priceSum+" 元"));
resultEl.appendChild(liNode)
});
})
//清楚父节点下的子节点元素
function cleanAndrender(parentEl){
var first = parentEl.firstElementChild;
while(first){
first.remove();
first = parentEl.firstElementChild;
}
}
//快速排序
function quickSort(arr, start, end){
if(start < end){
var pivotIndex = quickSortHelper(arr, start, end);
quickSort(arr, start, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, end);
}
}
function quickSortHelper(arr, start, end){
var pivot = sum(arr[start].map(item=>item.foodPrice))
var i = start;
var j = end;
while(i < j){
while(i < j && sum(arr[j].map(item=>item.foodPrice)) >= pivot){
j--;
}
while(i < j && sum(arr[i].map(item=>item.foodPrice)) <= pivot){
i++;
}
swap(arr, i, j);
}
swap(arr, i, start);
return i;
}
function swap(arr,i,j){
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
</script>
</body>
</html>