完成LeetCode【螺旋矩阵Ⅱ】这道题之后,我尝试用HTML+CSS+JS动画来还原算法过程。

228 阅读2分钟

前言

这几天在看LeetCode,发现了 螺旋矩阵Ⅱ 这道标记为中等难度的题,如下所示。

给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
示例
spiraln.jpg
输入: n = 3
输出: [[1,2,3],[8,9,4],[7,6,5]]

但它并没有涉及到什么算法,就是一个简单的模拟过程。 从初始位置即矩阵Matrix[0][0]开始填充数字,初始方向向右,控制列向量变化。然后再判断下一个位置即Matrix[0][1]是否为边界或已经被填充,若否,则继续填充,若是,则转换方向,控制行向量变化。之后反复操作即可。

题解如下:

/**
 * @param {number} n
 * @return {number[][]}
 */
var generateMatrix = function(n) {
    let res = new Array(n).fill(0).map(() => new Array(n).fill(0));
    
    let maxNum = n * n
    let nowNum = 1;
    let currentX = 0,currentY = 0;
    let directions = [[0,1],[1,0],[0,-1],[-1,0]] // 右下左上
    let moveIndex = 0

    while(nowNum <= maxNum){
        res[currentX][currentY] = nowNum ++
        let nextX = currentX + directions[moveIndex][0]
        let nextY = currentY + directions[moveIndex][1]
        if(nextX >= n || nextY >= n || nextX < 0 || nextY < 0 || res[nextX][nextY]!=0)
            moveIndex = (moveIndex + 1 ) % 4
        currentX = currentX + directions[moveIndex][0]
        currentY = currentY + directions[moveIndex][1]
    }
    return res
};

当然,今天并不是来分享这道题是怎么解的。我还用HTML+CSS+JS把刚才的过程 “可视化” 了。

思路

  1. 输入矩阵维度n
  2. 根据算法生成结果矩阵即数组resMatrix,并将每一步的结果保存到resObj对象中。
  3. 使用Grid布局将wrap分割成n * n的大小相同的网格。
  4. wrap中生成n * n个box,将其透明度设为0,并与resMatrix中的值一一对应。
  5. index初始值为1,使用setInterval控制index,若box的文本值等于index,则将box显示出来,设置对应的背景颜色,并添加简单的动画效果。resObj使得同一个方向中的box背景颜色相同。

实现效果

动画.gif GIF工具 ScreenToGif

完整代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>螺旋矩阵Ⅱ</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        body {
            width: 100vw;
            height: 100vh;
            background-color: #bdc3c7;
            display: flex;
            justify-content: center;
            align-items: center;
            flex-direction: column;
        }

        input {
            width: 500px;
            padding: 20px;
            font-size: 20px;
            box-sizing: border-box;
            border: none;
            outline: none;
        }

        .main {
            margin-top: 50px;
            display: flex;
            justify-content: center;
            position: relative;
        }

        .wrap {
            width: 500px;
            height: 500px;
            display: grid;
            gap: 5px;
            position: relative;
            overflow: hidden;
        }

        .wrap-trans::before {
            content: "";
            position: absolute;
            width: 50px;
            height: 100%;
            background: linear-gradient(0deg,rgba(255,255,255,0),rgba(255,255,255,0.5),rgba(255,255,255,0));
            z-index: 2;
            animation: twinkle .5s forwards;
        }

        .message {
            width: 300px;
            max-height: 500px;
            overflow-y: scroll;
            box-sizing: border-box;
            padding: 0 30px;
            position: absolute;
            right: -300px;
            color: #34495e;
            font-size: 12px;
            line-height: 30px;
        }

        /* 设置滚动条样式 */
        /* 滚动条整体样式 */
        .message::-webkit-scrollbar{
            width: 5px;
            height: 1px;
        }
        /* 滚动条里面的小方块 */
        .message::-webkit-scrollbar-thumb{
            border-radius: 4px;
            box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
            background: #bdc3c7;
        }

        .box {
            display: flex;
            justify-content: center;
            align-items: center;
            background-color: #fff;
            font-size: 24px;
            color: #34495e;
            font-weight: bold;
        }

        .showAnimation {
            animation-name: showBox;
            animation-duration: .5s;
            animation-fill-mode: forwards;
        }

        @keyframes showBox {
            from{
                opacity: 0;
            }
            to{
                opacity: 1;
            }
        }

        @keyframes twinkle {
            from{
                transform: translateX(0);
            }
            to{
                transform: translateX(520px);
            }
        }
    </style>
</head>
<body>
    <input type="text" id="inputEl" placeholder="请输入矩阵维度(1-10)...">
    <div class="main">
        <div class="wrap"></div>
        <div class="message"></div>
    </div>

    <script>
        const DELAY = 100
        const colors = ['#1abc9c','#f1c40f','#e84393','#e67e22','#3498db','#e74c3c','#9b59b6','#fab1a0','#95a5a6']

        let resMatrix;
        let resObj

        inputEl.onkeyup = function(e){
            var e = e || window.event
            if(e.keyCode == 13){
                let n = parseInt(inputEl.value)
                if(n >= 1 && n <= 10){
                    let wrap = document.querySelector('.wrap')
                    resMatrix = createMatrix(n)
                    clearDom(wrap)
                    updateGrid(wrap,n)
                    createDom(wrap,n)
                    showBox(n)
                }else
                    alert('请输入1-10之间的整数')
            }
        }

        function updateGrid(dom,n){
            dom.style.gridTemplateRows = `repeat(${n},1fr)`
            dom.style.gridTemplateColumns = `repeat(${n},1fr)`
        }

        function clearDom(dom){
            while(dom.hasChildNodes()){
                dom.removeChild(dom.firstChild);
            }            
        }

        function createDom(dom,n){
            for(let i=0;i<n;i++){
                for(let j=0;j<n;j++){
                    let box = document.createElement('div')
                    box.innerHTML = resMatrix[i][j]
                    box.classList.add('box')
                    // box.setAttribute('data-index',resMatrix[i][j])
                    box.style.opacity = '0'
                    dom.appendChild(box)
                }
            }
        }

        function showBox(n){
            let boxArray = Array.from(document.querySelectorAll('.box'))
            let messageBox = document.querySelector('.message')
            let index = 1

            messageBox.innerText = ""

            let timer = setInterval(() => {
                if(index == n*n){
                    clearInterval(timer);
                    document.querySelector('.wrap').classList.add('wrap-trans')
                    setTimeout(() => {
                        document.querySelector('.wrap').classList.remove('wrap-trans')
                    }, 1000);
                }
                
                let nowBox = boxArray.filter(item => {
                    // return item.getAttribute('data-index') == index
                    return item.innerHTML == index
                })
                nowBox[0].classList.add('showAnimation')
                nowBox[0].style.background = colors[resObj[index] % colors.length]
                messageBox.innerText += `正在填充${index},调整方向${resObj[index]}次。\n`
                messageBox.scrollTop = messageBox.scrollHeight;
                index ++
            }, DELAY);
        }

        function createMatrix(n){
            let res = new Array(n).fill(0).map(() => new Array(n).fill(0));

            resObj = {}
            let maxNum = n * n
            let nowNum = 1;
            let currentX = 0,currentY = 0;
            let directions = [[0,1],[1,0],[0,-1],[-1,0]] // 右下左上
            let moveIndex = 0
            let step = 0

            while(nowNum <= maxNum){
                res[currentX][currentY] = nowNum ++
                let nextX = currentX + directions[moveIndex][0]
                let nextY = currentY + directions[moveIndex][1]
                if(nextX >= n || nextY >= n || nextX < 0 || nextY < 0 || res[nextX][nextY]!=0){
                    moveIndex = (moveIndex + 1 ) % 4
                    step ++
                    resObj[nowNum-1] = step
                }else{
                    resObj[nowNum-1] = step
                }
                currentX = currentX + directions[moveIndex][0]
                currentY = currentY + directions[moveIndex][1]
            }
            return res
        }

    </script>
</body>
</html>

结语

上面实现的是先一次性生成所有的box并隐藏起来,通过定时器再显示出来。但在写的时候,由于一开始没有控制维度的大小,不小心输入了123,导致页面直接卡住了😂

那其实还有另外一种想法——逐个生成box,轮到你了你再来。但这样的结果是,由于box是按照顺序排放的,第一行排完了,直接从第二行第一列开始,这样就没法达到想要的效果,当然好像也可以通过grid-area来为每一个box固定位置,这可能也是一种思路,但我设置的最大维度为10,这样一来在速度上两者可能也没有多大差别哈哈。