前言
最近,帮一个同学编写了一个可视化的停车场管理系统程序,用于辅助他们的管理学的 pre。所以这个小项目也非常的简单,刚好运用了之前算法设计与分析与 WEB 技术的结合。既然是可视化程序,第一反应可以使用 C++,但是恰好上学期学习了 JavaScript,于是就用 JavaScript 编写这个程序。
项目在线 demo 地址:https://park.hoyue.eu.org/
信息描述
对于一个有限的停车场空间内,有一个固定的入口和出口。例子中设置为 4 列车位,除了最左侧的一列有 8 个车位外,每一列有 7 个车位。 入口和出口在同一个位置(1,0) ,每一列从左往右依次以 A0/B1/C2/D3 开头命名车位。根据纵坐标,也依次从 0 开始向上递增。概念图如下:

我们抽象成表格,就是如下的情况:
| y \ x | A0 | B1 | C2 | D3 |
|---|---|---|---|---|
| 8 | ||||
| 7 | ||||
| 6 | ||||
| 5 | ||||
| 4 | ||||
| 3 | ||||
| 2 | ||||
| 1 | ||||
| 0 | Entrance |
这是按照坐标轴的表格,其中如果这个位置是可用的,那么它的背景颜色就是 绿色(green)。如果这个位置是无效的,那么它的背景颜色就是 海蓝宝石色(aquamarine)。如果它的背景颜色是 红色(red) 的,那么表示该位置已经被占用,存在车辆了。
现在需要实现如下功能:
- 车辆停入:输入入场时间和车牌号,将车辆停入 距离入口最近 的且 未被占用 的车位,并显示停入信息。
- 车辆驶离:输入出场时间和车牌号, 释放被占用的车位,并输出驶离信息。
- 费用计算:输出停车产生的费用,该例子中为比较简单的
(t-1)×1。 - 表格显示:在表格上可视化展示停车位情况,在被占用的停车位中需要写入车牌号。
问题分析
距离最短问题
对于这个问题,还是比较简单解决的。首先是解决距离问题。
因为要求中, 每个格子距离起点的距离是固定的,我们不必每次停入的时候都重新计算找最小值,只需要一直维护一个容易保持有最小值的数据结构就可以了。于是很容易想到 使用一个最小堆去存储这些距离。
对于在 JavaScript 中实现堆,只需要稍微根据《Introduction to Algorithm》中的伪代码进行修改即可。
信息存储
这个问题中有很多信息,该如何存储呢?我想到的是使用两个类。
- 一个类是 Heap,用于维护堆数组。它有属性 x 和 y 可以用于定位单元格,有一个属性 distance 用于记录距离,这是堆数组的排列依据。
- 另一个类 Position 是用来维护车位(单元格),有属性 status 用于记录当前单元格的状态,分为 “occupied”(已占用)、“available”(可用)和 “null”(无效),一个属性 name 用于记录车辆信息,若车位非占用状态则为空,一个属性 t 用来记录车辆的入场时间。
初始情况下,定义一个一维数组 heap 作为 Heap 类的变量,定义一个二维数组 pos 作为 Position 类的变量。根据表格现有信息,初始化这两个数组即可。
入场问题
当车入场时,要做的事很简单。首先就是取到最短可用的车位的坐标,通过获取堆首的信息中 x, y 属性就可以知道它的坐标。然后在单元格数组 pos 中,标记指定位置,填入特定信息即可。
出场问题
当车出场时,若输入的车牌在停车场中,那么就恢复指定单元格位置,并且输出指定信息。我们可以在每次删去堆的时候,将其放到数组的末尾,这样出场的时候只需要匹配 heapSize + 1 到 SIZE 即可。
代码
代码按照部分展示在本文中,全文代码请看:https://github.com/Aki-Hoyue/parking_system
初始化
堆实现
这里是参考《Introduction to Algorithm》的堆实现部分的 JavaScript 代码。
伪代码部分以及分析请见:
https://hoyue.fun/algorithm_4.html
首先是基本堆函数:
function heapParent(i) { return Math.floor(i / 2);}
function left(i) { return i * 2;}
function right(i) { return i * 2 + 1;}维护最小堆:
function minHeapify(A, i) { var l = left(i); var r = right(i); var smallest = i;
if (l <= heapSize && A[l].distance < A[i].distance) smallest = l;
if (r <= heapSize && A[r].distance < A[smallest].distance) smallest = r;
if (smallest !== i) { var temp = A[i]; A[i] = A[smallest]; A[smallest] = temp; minHeapify(A, smallest); }}建堆:
function buildMinHeap(A, n) { for (var i = Math.floor(n / 2); i >= 1; i--) { minHeapify(A, i); }}删除堆首并放到堆后:
function extractMinFromHeap(A) { if (heapSize < 1) throw new Error("heap underflow");
const minElement = A[[1]](file://pointer_c3.xml); A[[1]](file://pointer_c3.xml) = A[heapSize]; A[heapSize] = minElement; heapSize --; minHeapify(A, 1); return minElement;}插入进堆:
function insertHeap(A,index){ if (heapSize == SIZE) throw new Error("heap overflow"); const element = A[heapSize + 1]; A[++heapSize] = A[index]; A[index] = element; for (var i = Math.floor(heapSize / 2); i >= 1; i--) { minHeapify(A, i); }}这是一个非常经典的算法导论的 JavaScript 实现,接下来就是把堆当做数据结构去使用堆。
堆初始化
var heapSize = 0;class Heap{ constructor(x,y) { this.x = x; this.y = y; this.distance = Math.sqrt((x-1)*(x-1) + y * y); }}
var heap = new Array(4 * 9);heap[0] = -1;var index = 1;heap[index++] = new Heap(0, 1);for (var x = 0; x < 4; x++) { for (var y = 2; y < 9; y++) { heap[index++] = new Heap(x, y); }}heapSize = index - 1;const SIZE = heapSize;buildMinHeap(heap,heapSize);设置 Heap 类的三个属性 x, y 和 distance,distance 会根据 x, y 的值自动计算,初始化 0 位置为一个非常小的值(这里因为距离是正数就设置为 -1)。index 作为堆数组的索引,创建每个位置上的 Heap 类元素。
设置一个常量 SIZE,作为最大的情况,之后检测的时候用到。
单元格初始化
单元格就简单很多,根据之前的表格初始化即可:
| y \ x | A0 | B1 | C2 | D3 |
|---|---|---|---|---|
| 8 | ||||
| 7 | ||||
| 6 | ||||
| 5 | ||||
| 4 | ||||
| 3 | ||||
| 2 | ||||
| 1 | ||||
| 0 | Entrance |
class Position { constructor(status, name, t) { this.status = status; this.name = name; this.t = t; }}
var pos = new Array(4);for (var x = 0; x < 4; x++) { pos[x] = new Array(9); for (var y = 0; y < 9; y++) { if (!y || (y == 1 && x)) { pos[x][y] = new Position("null", "", ""); continue; } pos[x][y] = new Position("available", "", ""); }}当页面被加载的时候,还需要初始化单元格的背景颜色:
function loding(){ var table = document.getElementById("ParkingArea"); for(var i = 0; i < 9; i++) // y for(var j = 0; j < 4; j++) // x { var cell = table.rows9-i].cells+1]; if(cell.innerHTML == "Entrance") continue; if(pos[j][i].status == "null") cell.style.backgroundColor = "aquamarine"; else cell.style.backgroundColor = "green"; }}入场
我们需要考虑一些意外条件,先行判断:
- 堆(停车场)是否已满。
- 输入的入场时间是否符合标准。
- 车牌是否为空或者已经停入。
我们需要先特判这些问题:
- 对于堆是否已满,只需要检查
heapSize的大小(heapSize < 1则已满)即可。 - 对于时间的格式,我们提示用户应该输入格式为 “yyyy/mm/dd hh:mm
”,并且使用正则表达式来检查用户的输入。 - 对于车牌,首先先判断是否为空,再在
heap数组中heapSize + 1到SIZE之间(这之间的是已经入场的位置)查找是否有相同的车牌。
于是代码为:
function input(){ if (heapSize < 1) { window.alert("The parking area is full!"); throw new Error("heap overflow."); } var entranceTime = document.getElementById("entrance_time"); var entranceName = document.getElementById("entrance_name"); var acTest = /^([0-9]{4})\/([0-1][0-9])\/([0-3][0-9]) ([0-2][0-9]):([0-5][0-9]):([0-5][0-9])$/; if(!acTest.test(entranceTime.value)) { window.alert("The time is not formatted correctly!"); throw new Error("Time format error."); } if(!entranceName.value) { window.alert("The name of license could not be empty!"); throw new Error("name error."); }
for(var i = heapSize + 1; i <= SIZE; i++) { var temp = heap[i]; if(pos[temp.x][temp.y].name == entranceName.value) { window.alert("The car has already been parked!"); throw new Error("repark error."); } }
var parkArea = extractMinFromHeap(heap); pos[parkArea.x][parkArea.y].status = 'occupied'; pos[parkArea.x][parkArea.y].name = entranceName.value; pos[parkArea.x][parkArea.y].t = entranceTime.value;
var table = document.getElementById("ParkingArea"); var cell = table.rows[9-parkArea.y].cells[parkArea.x+1]; cell.style.backgroundColor = "red"; cell.innerHTML = entranceName.value; cell.style.color = "green";
var info = document.getElementById("info_box"); info.innerHTML = "<b>" + entranceName.value + "</b> parks at <b>" + String.fromCharCode(parkArea.x + 65) + parkArea.x + parkArea.y + "</b>";}这个代码会检查停车场是否已满;检查时间、车牌格式;判重后取出最近的可用车位,标记为占用,并更新视图与信息面板。
出场
对于出场,我们同样考虑如下特殊情况:
- 堆(停车场)是否已满。
- 输入的出场时间是否符合标准、是否晚于入场时间。
- 车牌是否为空以及是否存在。
解决方法与入场类似,时间差用于计算费用(要求出场时间必须晚于入场时间)。
function output(){ var time = document.getElementById("out_time"); var acTest = /^([0-9]{4})\/([0-1][0-9])\/([0-3][0-9]) ([0-2][0-9]):([0-5][0-9]):([0-5][0-9])$/; if(!acTest.test(time.value)) { window.alert("The time is not formatted correctly!"); throw new Error("Time format error."); } var outTime = new Date(time.value); var outName = document.getElementById("out_name"); if(!outName.value) { window.alert("The name of license could not be empty!"); throw new Error("name error."); }
var flag = 0; for(var i = heapSize + 1; i <= SIZE; i++) { var temp = heap[i]; if(pos[temp.x][temp.y].status == 'occupied' && pos[temp.x][temp.y].name == outName.value) { flag = 1; var inTime = new Date(pos[temp.x][temp.y].t); var diff = (outTime.getTime() - inTime.getTime()) / (1000 * 60 * 60); if(diff < 0) { window.alert("The exit time must not be less than the entry time!"); throw new Error("Time error."); } var fee = wealth(Math.ceil(diff));
var table = document.getElementById("ParkingArea"); var cell = table.rows9-temp.y].cells.x+1]; cell.style.backgroundColor = "green"; cell.innerHTML = ""; var info = document.getElementById("info_box"); info.innerHTML = "<b>" + outName.value + "</b> was parked for <b>" + Math.floor(diff) + "hours and " + Math.round((diff - Math.floor(diff)) * 60) + "minutes</b> from " + pos[temp.x][temp.y].t + " to " + time.value + "<br>Fee: <b>" + fee + "</b> yuan";
pos[temp.x][temp.y].name = ""; pos[temp.x][temp.y].t = ""; pos[temp.x][temp.y].status = "available"; insertHeap(heap,i); } } if(!flag) { window.alert("License mismatch!"); throw new Error("No parking."); }}费用结算
费用计算就比较简单了,计算公式就是 1 小时免费,超过一小时收费 (t-1) 元。代码如下:
function wealth(t) { return (t > 0)? (t - 1) : 0;}HTML
最后在 HTML 页面中,我设计了左右结构的视图,如下图所示:

<!DOCTYPE html><html><head> <title>Parking System</title> <link rel="stylesheet" href="style.css"> <script src="parking.js" async></script> <meta http-equiv="Content-Script-Type" content="text/javascript" /></head>
<body onload="loding()"> <table> <tr> <td width="50%"> <table id="ParkingArea"> <caption>Parking Area</caption> <tr> <th></th> <th>A / 0</th> <th>B / 1</th> <th>C / 2</th> <th>D / 3</th> </tr> <tr> <th>8</th> <td></td> <td></td> <td></td> <td></td> </tr> <tr> <th>7</th> <td></td> <td></td> <td></td> <td></td> </tr> <tr> <th>6</th> <td></td> <td></td> <td></td> <td></td> </tr> <tr> <th>5</th> <td></td> <td></td> <td></td> <td></td> </tr> <tr> <th>4</th> <td></td> <td></td> <td></td> <td></td> </tr> <tr> <th>3</th> <td></td> <td></td> <td></td> <td></td> </tr> <tr> <th>2</th> <td></td> <td></td> <td></td> <td></td> </tr> <tr> <th>1</th> <td></td> <td></td> <td></td> <td></td> </tr> <tr> <th>0</th> <td></td> <td id="entrance_point">Entrance</td> <td></td> <td></td> </tr> </table> </td> <td width="50%"> <h2>Dashboard</h2> <form id="parking_form"> <fieldset> <legend>Enter</legend> <label for="entrance_time">Entrance Time:</label> <input type="text" id="entrance_time" name="entrance_time" maxlength="80" placeholder="yyyy/mm/dd hh:mm:ss" size="30" required><br> <label for="entrance_name" >License:</label> <input type="text" id="entrance_name" name="entrance_name" maxlength="30" size="30" required><br> <input type="button" value="Park" onclick="input()"> </fieldset> <fieldset> <legend>Exit</legend> <label for="out_time">Exit Time:</label> <input type="text" id="out_time" name="out_time" maxlength="80" placeholder="yyyy/mm/dd hh:mm:ss" size="30" required><br> <label for="out_name">License:</label> <input type="text" id="out_name" name="out_name" maxlength="30" size="30" required><br> <input type="button" value="Check Out" onclick="output()"> </fieldset> <fieldset> <legend>Output</legend> <label id="info_box">Information here...</label><br><br> <label>Notice: The format of the time must be "<em>yyyy/mm/dd hh:mm:ss</em>"</label><br> <label>Comment: a cell in <span style="color: aquamarine;">aquamarine</span> is <em>unavailable</em>, and that in <span style="color: green;">green</span> is <em>available</em>, in <span style="color: red;">red</span> is <em>occupied</em>.</label><br> <input type="reset" value="Clear"> </fieldset> </form> </td> </tr> </table></body></html>CSS
这个 CSS 设计得就比较简单了,如下:
#ParkingArea{ width: 100%; border-collapse: collapse; float: left;}
table caption{ font-size: 2em; font-weight: bold; margin: 1em 0;}
#ParkingArea th,#ParkingArea td{ border: 1px solid black; text-align: center; padding: 14px 0;}
#ParkingArea tbody tr{ background-color: #eee;}
#ParkingArea th{ background-color:cadetblue; color:whitesmoke;}
#entrance_point{ background-color: goldenrod; color: purple;}
h2{ font-size: 2em; font-weight: bold; text-align: center;}
form{ font-size: 14pt;}后记
项目在线 demo 地址:https://park.hoyue.eu.org/
好了,这个小项目就简单地完成了,这个项目联系了算法与 WEB 技术,是个不错的小实践。