数据结构之----队列

155 阅读4分钟

1、队列的定义

队列是一种特殊的线性表,其特殊之处在于,它只允许你在队列的头部删除元素,在队列的末尾添加新的元素。

队列的方法如下:

  • enqueue 从队列尾部添加一个元素(新来一个排队的人,文明礼貌,站在了队伍末尾)
  • dequeue 从队列头部删除一个元素(排队伍最前面的人刚办理完登机手续,离开了队伍)
  • head 返回头部的元素,注意,不是删除(只是看一下,谁排在最前面)
  • size 返回队列大小(数一数有多少人在排队)
  • clear 清空队列(航班取消,大家都散了吧)
  • isEmpty 判断队列是否为空 (看看是不是有人在排队)
  • tail 返回队列尾节点

下面,逐一实现这些方法:

function Queue(){
    var items = [];   // 存储数据

    // 向队列尾部添加一个元素
    this.enqueue = function(item){
        items.push(item);
    };

    // 移除队列头部的元素
    this.dequeue = function(){
        return items.shift();
    };

    // 返回队列头部的元素
    this.head = function(){
        return items[0];
    }

    // 返回队列大小
    this.size = function(){
        return items.length;
    }

    // clear
    this.clear = function(){
        items = [];
    }

    // isEmpty 判断是否为空队列
    this.isEmpty = function(){
        return items.length == 0;
    }
};

1 约瑟夫环

有一个数组a[100]存放0--99;要求每隔两个数删掉一个数,到末尾时循环至开头继续进行,求最后一个被删掉的数。

前10个数是 0 1 2 3 4 5 6 7 8 9 10,所谓每隔两个数删掉一个数,其实就是把 2 5 8 删除掉,如果只是从0 到 99 每个两个数删掉一个数,其实挺简单的,可是题目要求到末尾时还有循环至开头继续进行。

如果是用数组,问题就又显得麻烦了,关键是到了末尾如何回到开头重新来一遍,还得考虑把删除掉的元素从数组中删除。

如果用队列就简单了,先将这100个数放入队列,使用while循环,while循环终止的条件是队列里只有一个元素。使用index变量从0开始计数,算法步骤如下:

  1. 从队列头部删除一个元素,index+1
  2. 如果index%3 == 0,就说明这个元素是需要删除的元素,如果不等于0,就不是需要被删除的元素,则把它添加到队列的尾部

不停的有元素被删除,最终队列里只有一个元素,此时while循环终止,队列的所剩的元素就是最后一个被删除的元素。

function del_ring(arr_list){
    // 把数组里的元素都放入到队列中
    var queue = new Queue();
    for(var i=0;i< arr_list.length;i++){
        queue.enqueue(arr_list[i]);
    }

    var index = 0;
    while(queue.size() != 1){
        // 弹出一个元素,判断是否需要删除
        var item = queue.dequeue();
        index += 1;
        // 每隔两个就要删除掉一个,那么不是被删除的元素就放回到队列尾部
        if(index %3 != 0){
            queue.enqueue(item);
        }
    }

    return queue.head();
};

// 准备好数据
var arr_list = [];
for(var i=0;i< 100;i++){
    arr_list.push(i);
}


console.log(del_ring(arr_list));

2 斐波那契数列

斐波那契数列是一个非常经典的问题,有着各种各样的解法,比较常见的是递归算法,其实也可以使用队列来实现

斐波那契数列的前两项是 1 1 ,此后的每一项都是该项前面两项之和,即f(n) = f(n-1) + f(n-2)。

如果使用数组来实现,究竟有多麻烦了我就不赘述了,直接考虑使用队列来实现。

先将两个1 添加到队列中,之后使用while循环,用index计数,循环终止的条件是index < n -2

  • 使用dequeue方法从队列头部删除一个元素,该元素为del_item
  • 使用head方法获得队列头部的元素,该元素为 head_item
  • del_item + head_item = next_item,将next_item放入队列,注意,只能从尾部添加元素
  • index+1

当循环结束时,队列里面有两个元素,先用dequeue 删除头部元素,剩下的那个元素就是我们想要的答案

使用队列计算斐波那契数列的第n项

    queue = new Queue();
    var index = 0;
    // 先放入斐波那契序列的前两个数值
    queue.enqueue(1);
    queue.enqueue(1);
    while(index < n-2){
        // 出队列一个元素
        var del_item = queue.dequeue();
        // 取队列头部元素
        var head_item = queue.head();
        var next_item = del_item + head_item;
        // 将计算结果放入队列
        queue.enqueue(next_item);
        index += 1;
    }

    queue.dequeue();
    return queue.head();
};


console.log(fibonacci(8));

3 用队列实现栈

用两个队列实现一个栈

队列是先进先出,而栈是先进后出,两者对数据的管理模式刚好是相反的,但是却可以用两个队列实现一个栈。

两个队列分别命名为queue_1, queue_2,实现的思路如下:

  • push, 实现push方法时,如果两个队列都为空,那么默认向queue_1里添加数据,如果有一个不为空,则向这个不为空的队列里添加数据

  • top,两个队列,或者都为空,或者有一个不为空,只需要返回不为空的队列的尾部元素即可

  • pop,pop方法是比较复杂,pop方法要删除的是栈顶,但这个栈顶元素其实是队列的尾部元素。每一次做pop操作时,将不为空的队列里的元素一次删除并放入到另一个队列中直到遇到队列中只剩下一个元素,删除这个元素,其余的元素都跑到之前为空的队列中了。

在具体的实现中,我定义额外的两个变量,data_queue和empty_queue,data_queue始终指向那个不为空的队列,empty_queue始终指向那个为空的队列。

function QueueStack(){
    var queue_1 = new Queue();
    var queue_2 = new Queue();
    var data_queue = null;      // 放数据的队列
    var empty_queue = null;     // 空队列,备份使用

    // 确认哪个队列放数据,哪个队列做备份空队列
    var init_queue = function(){
        // 都为空,默认返回queue_1
        if(queue_1.isEmpty() && queue_2.isEmpty()){
            data_queue = queue_1;
            empty_queue = queue_2;
        }else if(queue_1.isEmpty()){
            data_queue = queue_2;
            empty_queue = queue_1;
        }else{
            data_queue = queue_1;
            empty_queue = queue_2;
        }
    };


    // push方法
    this.push = function (item) {
        init_queue();
        data_queue.enqueue(item);
    };

    // top方法
    this.top = function(){
        init_queue();
        return data_queue.tail();
    }

    /**
     * pop方法要弹出栈顶元素,这个栈顶元素,其实就是queue的队尾元素
     * 但是队尾元素是不能删除的,我们可以把data_queue里的元素(除了队尾元素)都移除放入到empty_queue中
     * 最后移除data_queue的队尾元素并返回
     * data_queue 和 empty_queue 交换了身份
     */
    this.pop = function(){
        init_queue();
        while(data_queue.size()>1){
            empty_queue.enqueue(data_queue.dequeue());
        }
        return data_queue.dequeue();
    };
};


var q_stack = new QueueStack();
q_stack.push(1);
q_stack.push(2);
q_stack.push(4);
console.log(q_stack.top());   // 栈顶是 4
console.log(q_stack.pop());   // 移除 4
console.log(q_stack.top());   // 栈顶变成 2
console.log(q_stack.pop());   // 移除 2
console.log(q_stack.pop());   // 移除 2

4 打印杨辉三角(困难模式)

使用队列打印出杨辉三角的前n行,n >= 1

杨辉三角中的每一行,都依赖于上一行,假设在队列里存储第n - 1行的数据,输出第n行时,只需要将队列里的数据依次出队列,进行计算得到下一行的数值并将计算所得放入到队列中。

计算的方式:f[i][j] = f[i-1][j-1] + f[i-1][j], i 代表行数,j代表一行的第几个数,如果j= 0 或者 j = i ,则 f[i][j] = 1。

但是将计算所得放入到队列中时,队列中保存的是两行数据,一部分是第n-1行,另一部分是刚刚计算出来的第n行数据,需要有办法将这两行数据分割开。

分开的方式有两种,一种是使用for循环进行控制,在输出第5行时,其实只有5个数据可以输出,那么就可以使用for循环控制调用enqueue的次数,循环5次后,队列里存储的就是计算好的第6行的数据。

第二种方法是每一行的数据后面多存储一个0,使用这个0来作为分界点,如果enqueue返回的是0,就说明这一行已经全部输出,此时,将这个0追加到队列的末尾。

function print_yanghui(n){
    var queue = new Queue();
    queue.enqueue(1);
    // 第一层for循环控制打印几层
    for(var i=1; i<=n; i++){
        var line = "";
        var pre = 0;
        // 第二层for循环控制打印第 i 层
        for(var j=0; j<i; j++){
            var item = queue.dequeue();
            line += item + "  "
            // 计算下一行的内容
            var value = item + pre;
            pre = item;
            queue.enqueue(value);
        }
        // 每一层最后一个数字是1,上面的for循环没有计算最后一个数
        queue.enqueue(1);
        console.log(line);
    }
};

function print_yanghui_2(n){
    var queue = new Queue();
    queue.enqueue(1);
    queue.enqueue(0);
    for(var i=1; i<=n; i++){
        var line = "";
        var pre = 0;
        while(true){
            var item = queue.dequeue();
            // 用一个0把每一行的数据分割开,遇到0不输出,
            if(item==0){
                queue.enqueue(1);
                queue.enqueue(0);
                break
            }else {
                // 计算下一行的内容
                line += item + "  "
                var value = item + pre;
                pre = item;
                queue.enqueue(value);
            }
        }
        console.log(line);
    }
}

print_yanghui(10);
print_yanghui_2(10);

5 用两个栈实现一个队列(普通模式)

栈是先进后出,队列是先进先出,但可以用两个栈来模拟一个队列,请实现enqueue,dequeue, head这三个方法。

就这道题目而言,我们先考虑如何实现enqueue方法,两个栈分别命名为stack_1,stack_2,面对这两个栈,你能怎么做呢,似乎也只好选一个栈用来存储数据了,那就选stack_1来存储数据吧,看起来是一个无奈之举,但的确实现了enqueue方法。

接下来考虑dequeue方法,队列的头,在stack_1的底部,栈是先进后出,目前取不到,可不还有stack_2么,把stack_1里的元素都倒入到stack_2中,这样,队列的头就变成了stack_2的栈顶,这样不就可以执行stack_2.pop()来删除了么。执行完pop后,需要把stack_2里的元素再倒回到stack_1么,不需要,现在队列的头正好是stack_2的栈顶,恰好可以操作,队列的dequeue方法借助栈的pop方法完成,队列的head方法借助栈的top方法完成。

如果stack_2是空的怎么办?把stack_1里的元素都倒入到stack_2就可以了,这样,如果stack_1也是空的,说明队列就是空的,返回null就可以了。

enqueue始终都操作stack_1,dequeue和head方法始终都操作stack_2。

function StackQueue(){
    var stack_1 = new Stack();
    var stack_2 = new Stack();

    // 总是把数据放入到stack_1中
    this.enqueue = function(item){
        stack_1.push(item);
    };

    // 获得队列的头
    this.head = function(){
        // 两个栈都是空的
        if(stack_2.isEmpty() && stack_1.isEmpty()){
            return null;
        }

        // 如果stack_2 是空的,那么stack_1一定不为空,把stack_1中的元素倒入stack_2
        if(stack_2.isEmpty()){
            while(!stack_1.isEmpty()){
                stack_2.push(stack_1.pop());
            }
        }
        return stack_2.top();
    };

    // 出队列
    this.dequeue = function(){
        // 两个栈都是空的
        if(stack_2.isEmpty() && stack_1.isEmpty()){
            return null;
        }

        // 如果stack_2 是空的,那么stack_1一定不为空,把stack_1中的元素倒入stack_2
        if(stack_2.isEmpty()){
            while(!stack_1.isEmpty()){
                stack_2.push(stack_1.pop());
            }
        }
        return stack_2.pop();
    };

};


var sq = new StackQueue();
sq.enqueue(1);
sq.enqueue(4);
sq.enqueue(8);
console.log(sq.head());
sq.dequeue();
sq.enqueue(9);
console.log(sq.head());
sq.dequeue();
console.log(sq.head());
console.log(sq.dequeue());
console.log(sq.dequeue());

6 有一个二维数组

var maze_array = [[0, 0, 1, 0, 0, 0, 0],
                  [0, 0, 1, 1, 0, 0, 0],
                  [0, 0, 0, 0, 1, 0, 0],
                  [0, 0, 0, 1, 1, 0, 0],
                  [1, 0, 0, 0, 1, 0, 0],
                  [1, 1, 1, 0, 0, 0, 0],
                  [1, 1, 1, 0, 0, 0, 0]
                 ];

元素为0,表示这个点可以通行,元素为1,表示不可以通行,设置起始点为maze_array[2][1],终点是maze_array[3][5],请用程序计算这两个点是否相通,如果相通请输出两点之间的最短路径(从起始点到终点所经过的每一个点)

从起始点到终点,需要经过8个点,这是最短的连通路径。这时,要从终点开始反方向寻找路径,在终点的四周,一定存在一个点被标记为8,这个标记为8的点的四周一定存在一个点被标记为7,以此类推,最终找到标记为1的那个点,这个点的四周一定有一个点是起始点。

// 起始点是maze_array[2][1], 终点是 maze_array[3][5]
var maze_array = [[0, 0, 1, 0, 0, 0, 0],
    [0, 0, 1, 1, 0, 0, 0],
    [0, 0, 0, 0, 1, 0, 0],
    [0, 0, 0, 1, 1, 0, 0],
    [1, 0, 0, 0, 1, 0, 0],
    [1, 1, 1, 0, 0, 0, 0],
    [1, 1, 1, 0, 0, 0, 0]
];


var Node = function(x, y){
    this.x = x;
    this.y = y;
    this.step = 0;
};

var Position = function(x, y){
    this.x = x;
    this.y = y;
}


// 找到pos可以到达的点
function find_position(pos, maze){
    var x = pos.x;
    var y = pos.y;
    var pos_arr = [];
    // 上面的点
    if(x-1 >= 0){
        pos_arr.push(new Position(x-1, y));
    }
    // 右面的点
    if(y+1 < maze[0].length){
        pos_arr.push(new Position(x, y+1));
    }
    // 下面的点
    if(x+1 < maze.length){
        pos_arr.push(new Position(x+1, y));
    }
    // 左面的点
    if(y-1 >= 0){
        pos_arr.push(new Position(x, y-1));
    }
    return pos_arr;
};

function print_node(maze_node){
    for(var i = 0; i<maze_node.length;i++){
        var arr = [];
        for(var j =0;j<maze_node[i].length;j++){
            arr.push(maze_node[i][j].step);
        }
        console.log(arr);
    }
}

function find_path(maze, start_pos, end_pos){
    var maze_node = [];
    // 初始化maze_node,用于记录距离出发点的距离
    for(var i = 0; i< maze_array.length; i++){
        var arr = maze_array[i];
        var node_arr = [];
        for(var j =0; j< arr.length; j++){
            var node = new Node(i, j);
            node_arr.push(node);
        }
        maze_node.push(node_arr);
    }

    // 先把出发点放入到队列中
    var queue = new Queue();
    queue.enqueue(start_pos);
    var b_arrive = false;
    var max_step = 0;         // 记录从出发点到终点的距离
    while(true){
        // 从队列中弹出一个点,计算这个点可以到达的位置
        var position = queue.dequeue();
        var pos_arr = find_position(position, maze)
        for(var i =0; i<pos_arr.length; i++){
            var pos = pos_arr[i];
            // 判断是否到达终点
            if(pos.x == end_pos.x && pos.y==end_pos.y){
                b_arrive = true;
                max_step = maze_node[position.x][position.y].step;
                break;
            }

            // 起始点
            if(pos.x == start_pos.x && pos.y == start_pos.y){
                continue;
            }
            // 不能通过
            if(maze[pos.x][pos.y]==1){
                continue;
            }
            // 已经标识过步数
            if(maze_node[pos.x][pos.y].step > 0){
                continue;
            }
            // 这个点的步数加 1
            maze_node[pos.x][pos.y].step = maze_node[position.x][position.y].step + 1;
            queue.enqueue(pos);
        }
        //到达终点了
        if(b_arrive){
            break
        }

        // 栈为空,说明找不到
        if(queue.isEmpty()){
            break;
        }
    }

    // 方向查找路径
    var path = [];
    if(b_arrive){
        // 能够找到路径
        path.push(end_pos);
        var old_pos = end_pos;
        var step = max_step;
        while(step >0){
            var pos_arr = find_position(old_pos, maze);
            for(var i =0; i<pos_arr.length; i++) {
                var pos = pos_arr[i];

                if(maze_node[pos.x][pos.y].step == step){
                    step -= 1;
                    old_pos = pos;
                    path.push(pos);
                    break;
                }
            }
        }
        path.push(start_pos);
    }

    console.log(path.reverse());


};



var start_pos = new Position(2, 1);
var end_pos = new Position(3, 5);

find_path(maze_array, start_pos, end_pos);