算法與數(shù)據(jù)結(jié)構(gòu)-棧(Stack)-Java實(shí)現(xiàn)


title: 算法與數(shù)據(jù)結(jié)構(gòu)-棧(Stack)-Java實(shí)現(xiàn)

date: 2019-02-18 22:48:25

categories:

  • tech
  • data-structure
  • stack

tags: [tech,data-structure,stack,Java]


什么是棧(Stack)

下壓棧(FIFO queue),或者說棧(queue),是一種基于先進(jìn)后出策略的集合模型。

使用場(chǎng)景

只要你留心,就會(huì)發(fā)現(xiàn)棧這種數(shù)據(jù)結(jié)構(gòu)在生活中非常常見。

你在桌子上放了一摞文件,放文件和取文件就是簡單的棧操作。

你打開你的電子郵件賬戶,發(fā)現(xiàn)最新的郵件在最前面,如果這個(gè)時(shí)候有人給你發(fā)來新的郵件,你點(diǎn)擊收信,發(fā)現(xiàn)新來的郵件又在你未讀郵件列表的最上面,這就是入棧;你從上到下依次點(diǎn)開郵件閱讀,這些唯未讀郵件也就是一一從你的未讀郵件列表移除了,這就是出棧操作。

你點(diǎn)開一個(gè)網(wǎng)頁,然后再點(diǎn)擊網(wǎng)頁中的鏈接,這樣一直點(diǎn)擊下去,直到你想回退到前面的網(wǎng)頁了,你開始點(diǎn)擊回退按鈕,前面的網(wǎng)頁又一一出棧。同樣的,編輯器的回退功能,也是入棧出棧的例子。

Java實(shí)現(xiàn)

Stack

我們先定義棧的接口,一個(gè)完整的棧的接口,應(yīng)該包含如下四個(gè)方法,即:

  • 入棧
  • 出棧
  • 棧是否為空
  • 棧中元素?cái)?shù)量

下面是棧接口的定義:

package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack;

public interface Stack<Item> {

    /**
     * add an item
     *
     * @param item
     */
    void push(Item item);

    /**
     * remove the most recently added item
     *
     * @return
     */
    Item pop();

    /**
     * is the stack empty?
     *
     * @return
     */
    boolean isEmpty();

    /**
     * number of items in the stac
     */
    int size();

}

FixedCapacityStackOfStrings

首先我們實(shí)現(xiàn)一個(gè)最簡單的棧:定容棧,即容量固定的棧,棧的元素都為字符串。

一個(gè)棧的實(shí)現(xiàn)需要有盛棧元素的地方,我們使用數(shù)組。

還要有標(biāo)記當(dāng)前棧元素?cái)?shù)量的變量N。

package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;

import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;

import java.util.Iterator;
import java.util.Spliterator;
import java.util.function.Consumer;

/**
 * String定容棧:
 * 固定容量的String類型棧
 */
public class FixedCapacityStackOfStrings {

    private String[] a; // stack entries

    private int N;      // size

    public FixedCapacityStackOfStrings(int cap) {
        a = new String[cap];
    }

    public boolean isEmpty() {
        return N == 0;
    }

    public int size() {
        return N;
    }

    public void push(String item) {
        a[N++] = item;
    }

    public String pop() {
        return a[--N];
    }

}

  • 測(cè)試
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;

public class FixedCapacityStackOfStringsTests {

    public  static void main(String[] args){

        FixedCapacityStackOfStrings fixedCapacityStackOfStrings=new FixedCapacityStackOfStrings(10);
        System.out.println("fixedCapacityStackOfStrings : size="+fixedCapacityStackOfStrings.size()+",isEmpty="+fixedCapacityStackOfStrings.isEmpty());

        fixedCapacityStackOfStrings.push("A");
        fixedCapacityStackOfStrings.push("Aaha");
        System.out.println("fixedCapacityStackOfStrings : size="+fixedCapacityStackOfStrings.size()+",isEmpty="+fixedCapacityStackOfStrings.isEmpty());

        System.out.println("poped="+fixedCapacityStackOfStrings.pop());
        System.out.println("fixedCapacityStackOfStrings : size="+fixedCapacityStackOfStrings.size()+",isEmpty="+fixedCapacityStackOfStrings.isEmpty());

    }
}

  • 測(cè)試輸出
fixedCapacityStackOfStrings : size=0,isEmpty=true
fixedCapacityStackOfStrings : size=2,isEmpty=false
poped=Aaha
fixedCapacityStackOfStrings : size=1,isEmpty=false

FixedCapacityStack

FixedCapacityStackOfStrings的缺點(diǎn)是只能處理String對(duì)象,接著我們是使用泛型,讓我們的棧實(shí)現(xiàn)可以處理任意對(duì)象。

其中Item就是我們泛型的類型參數(shù)。

由于歷史原因,Java的數(shù)組一般情況下是不支持泛型的,因此我們用強(qiáng)轉(zhuǎn)的方式將Object類型的數(shù)組轉(zhuǎn)為泛型中的數(shù)組類型。

package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;

import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;

/**
 * 支持泛型的定容棧
 * @param <Item>
 */
public class FixedCapacityStack<Item> implements Stack<Item> {

    private Item[] a;

    private int N;

    public FixedCapacityStack(int cap){
        a = (Item[]) new Object[cap];
    }

    @Override
    public void push(Item item) {
        a[N++] = item;
    }

    @Override
    public Item pop() {
        return a[--N];
    }

    @Override
    public boolean isEmpty() {
        return N == 0;
    }

    @Override
    public int size() {
        return N;
    }

}

  • 測(cè)試
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;

public class FixedCapacityStackTests {

    public  static void main(String[] args){
        FixedCapacityStack<Double> fixedCapacityStack=new FixedCapacityStack<>(10);
        System.out.println("fixedCapacityStack : size="+fixedCapacityStack.size()+",isEmpty="+fixedCapacityStack.isEmpty());

        fixedCapacityStack.push(new Double(10.01));
        fixedCapacityStack.push(new Double(202.22));
        System.out.println("fixedCapacityStack : size="+fixedCapacityStack.size()+",isEmpty="+fixedCapacityStack.isEmpty());

        System.out.println("poped="+fixedCapacityStack.pop());
        System.out.println("fixedCapacityStack : size="+fixedCapacityStack.size()+",isEmpty="+fixedCapacityStack.isEmpty());

    }
}

  • 測(cè)試輸出
fixedCapacityStack : size=0,isEmpty=true
fixedCapacityStack : size=2,isEmpty=false
poped=202.22
fixedCapacityStack : size=1,isEmpty=false

ResizingArrayStack

FixedCapacityStack的最大缺點(diǎn)就是容量固定,這就要求我們?cè)谑褂脳V氨仨毠烙?jì)棧的最大容量,很不方便。

下面我們就實(shí)現(xiàn)容量可變的棧。

我們用一個(gè)新的數(shù)組來替換老的數(shù)組,從而實(shí)現(xiàn)棧的容量擴(kuò)展。這里要注意如兩點(diǎn):

  • 當(dāng)進(jìn)行入棧操作的時(shí)候,如果棧滿,則將其容量增大一倍,保證接下來可以多次入棧。因?yàn)轭l繁擴(kuò)展容量也是很耗費(fèi)內(nèi)存的。
  • 當(dāng)進(jìn)行出棧操作的時(shí)候,如果發(fā)現(xiàn)只用了棧容量的四分之一,則將棧的容量縮小一半。因?yàn)閿?shù)組如果空著不用,會(huì)白白耗費(fèi)內(nèi)存。

另外特別注意的是,出棧以后要將指定位置的元素賦值為null,以防止對(duì)象游離。

Java的垃圾回收策略是回收所有無法被訪問的對(duì)象的內(nèi)存,如果出棧以后,不將指定位置的元素賦值為null,那么保存這樣一個(gè)不需要的對(duì)象的引用,就稱為對(duì)象的游離。

通過賦值已經(jīng)出棧的位置為null,我們覆蓋了無效的引用,好讓GC回收這部分內(nèi)存。

package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;

import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;

/**
 * 容量可變的棧
 *
 * @param <Item>
 */
public class ResizingArrayStack<Item> implements Stack<Item> {

    private Item[] a = (Item[]) new Object[1];

    private int N = 0;

    /**
     * 改變棧的容量大小
     *
     * @param max
     */
    private void resize(int max) {
        // Move stack to a new array of size max.
        Item[] temp = (Item[]) new Object[max];
        for (int i = 0; i < N; i++) {
            temp[i] = a[i];
        }
        a = temp;
    }

    @Override
    public void push(Item item) {
        //如果棧滿,則將其容量增大一倍
        if (N == a.length) {
            resize(2 * a.length);
        }
        a[N++] = item;
    }

    @Override
    public Item pop() {
        Item item = a[--N];
        // 防止對(duì)象游離(loitering)
        a[N] = null;
        //如果棧中已用的容量只占總?cè)萘康?/4,則將棧容量縮小一半
        if (N > 0 && N == a.length / 4) {
            resize(a.length / 2);
        }
        return item;
    }

    @Override
    public boolean isEmpty() {
        return N == 0;
    }

    @Override
    public int size() {
        return N;
    }
}

  • 測(cè)試
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;

import java.math.BigDecimal;

public class ResizingArrayStackTests {
    public  static void main(String[] args){

        ResizingArrayStack<BigDecimal> resizingArrayStack=new ResizingArrayStack<>();

        System.out.println("resizingArrayStack : size="+resizingArrayStack.size()+",isEmpty="+resizingArrayStack.isEmpty());

        resizingArrayStack.push(new BigDecimal(100.001));
        resizingArrayStack.push(new BigDecimal(202.022));
        System.out.println("resizingArrayStack : size="+resizingArrayStack.size()+",isEmpty="+resizingArrayStack.isEmpty());

        System.out.println("poped="+resizingArrayStack.pop());
        System.out.println("resizingArrayStack : size="+resizingArrayStack.size()+",isEmpty="+resizingArrayStack.isEmpty());

    }
}

  • 測(cè)試輸出
resizingArrayStack : size=0,isEmpty=true
resizingArrayStack : size=2,isEmpty=false
poped=202.02199999999999135980033315718173980712890625
resizingArrayStack : size=1,isEmpty=false

IterableResizingArrayStack

下面我們將為我們的棧實(shí)現(xiàn)增加迭代器的特性。

事實(shí)上,foreach不僅僅是for的簡寫形式語法糖這么簡單,如下foreachwhile循環(huán)是等效的:

for(String s:collection){
    s ...
}
while(collection.hasNext()){
    collection.next();
    ...
}

從上面例子可以看出,迭代器其實(shí)就是一個(gè)實(shí)現(xiàn)了hasNext()next()方法的對(duì)象。

如果一個(gè)類可迭代,那么第一步就要聲明實(shí)現(xiàn)Iterable接口。

然后我們通過一個(gè)內(nèi)部類來實(shí)現(xiàn)Iterator的hasNext()next()方法從而支持迭代操作。

package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;

import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;

import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * 可迭代的可變長的基于數(shù)組存儲(chǔ)的棧實(shí)現(xiàn)
 *
 * @param <Item>
 */
public class IterableResizingArrayStack<Item> implements Stack<Item>, Iterable<Item> {

    private Item[] a = (Item[]) new Object[1];

    private int N = 0;

    /**
     * 改變棧的容量大小
     *
     * @param max
     */
    private void resize(int max) {
        // Move stack to a new array of size max.
        Item[] temp = (Item[]) new Object[max];
        for (int i = 0; i < N; i++) {
            temp[i] = a[i];
        }
        a = temp;
    }

    @Override
    public void push(Item item) {
        //如果棧滿,則將其容量增大一倍
        if (N == a.length) {
            resize(2 * a.length);
        }
        a[N++] = item;
    }

    @Override
    public Item pop() {
        Item item = a[--N];
        // 防止對(duì)象游離(loitering)
        a[N] = null;
        //如果棧中已用的容量只占總?cè)萘康?/4,則將棧容量縮小一半
        if (N > 0 && N == a.length / 4) {
            resize(a.length / 2);
        }
        return item;
    }

    @Override
    public boolean isEmpty() {
        return N == 0;
    }

    @Override
    public int size() {
        return N;
    }

    @Override
    public Iterator<Item> iterator() {
        return new ReverseArrayIterator();
    }

    //支持迭代方法,實(shí)現(xiàn)在內(nèi)部類里
    private class ReverseArrayIterator implements Iterator<Item> {
        // Support LIFO iteration.
        private int i = N;

        @Override
        public boolean hasNext() {
            return i > 0;
        }

        @Override
        public Item next() {
            if(i<=0){
                throw new NoSuchElementException();
            }

            return a[--i];
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
}

  • 測(cè)試
package net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl;

import java.math.BigDecimal;

public class IterableResizingArrayStackTests {
    public static void main(String[] args) {

        IterableResizingArrayStack<Float> resizingArrayStack = new IterableResizingArrayStack<>();

        System.out.println("resizingArrayStack : size=" + resizingArrayStack.size() + ",isEmpty=" + resizingArrayStack.isEmpty());

        resizingArrayStack.push(new Float(100.001));
        resizingArrayStack.push(new Float(202.022));
        System.out.println("resizingArrayStack : size=" + resizingArrayStack.size() + ",isEmpty=" + resizingArrayStack.isEmpty());

        System.out.println("resizingArrayStack all items:");
        for (Float f:resizingArrayStack) {
            System.out.println(f);
        }

        System.out.println("poped=" + resizingArrayStack.pop());
        System.out.println("resizingArrayStack : size=" + resizingArrayStack.size() + ",isEmpty=" + resizingArrayStack.isEmpty());

    }
}

  • 測(cè)試輸出
resizingArrayStack : size=0,isEmpty=true
resizingArrayStack : size=2,isEmpty=false
resizingArrayStack all items:
202.022
100.001
poped=202.022
resizingArrayStack : size=1,isEmpty=false

應(yīng)用示例

判斷括號(hào)是否為成對(duì)出現(xiàn)

要求一個(gè)字符串中,如果有括號(hào)的話,所有括號(hào),必須是成對(duì)出現(xiàn)的,且左括號(hào)必須在右括號(hào)之前出現(xiàn)。

寫一個(gè)檢查器檢查指定字符串是否符合上面的原則。

根據(jù)TDD(測(cè)試驅(qū)動(dòng)開發(fā))的開發(fā)方法,先把單元測(cè)試寫好:

package net.ijiangtao.tech.algorithms.algorithmall.algorithm.stack.evaluation;

import net.ijiangtao.tech.algorithms.algorithmall.algorithm.stack.checker.LegalParenthesesChecker;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class LegalParenthesesCheckerTests {

    @Test
    public void testChecker(){

        Assert.assertFalse(LegalParenthesesChecker.check("1}"));
        Assert.assertFalse(LegalParenthesesChecker.check("[1}"));
        Assert.assertFalse(LegalParenthesesChecker.check("[1]}"));
        Assert.assertFalse(LegalParenthesesChecker.check("(((1+1)+2)+3"));
        Assert.assertFalse(LegalParenthesesChecker.check("<((1+1)+2)+3"));

        Assert.assertTrue(LegalParenthesesChecker.check(""));
        Assert.assertTrue(LegalParenthesesChecker.check(" "));
        Assert.assertTrue(LegalParenthesesChecker.check("1"));
        Assert.assertTrue(LegalParenthesesChecker.check("[]"));
        Assert.assertTrue(LegalParenthesesChecker.check("[1]"));
        Assert.assertTrue(LegalParenthesesChecker.check("{(『((<1+1>)+【2】)+』3)}"));
    }

}

下面開始寫實(shí)現(xiàn)。

package net.ijiangtao.tech.algorithms.algorithmall.algorithm.stack.checker;

import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;
import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl.IterableResizingArrayStack;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * 括號(hào)必須成對(duì)出現(xiàn),此程序基于棧結(jié)構(gòu),用于檢測(cè)常用括號(hào)是否為成對(duì)出現(xiàn),并表達(dá)式輸出是否合法。
 */
public class LegalParenthesesChecker {

    private static final String BRACKET="<>()()〈〉??﹛﹜『』〖〗[]《》﹝﹞〔〕{}「」【】︵︶︷︸︿﹀︹︺︽︾﹁﹂﹃﹄︻︼";

    public static boolean check(String expression) {

        //如果傳入的表達(dá)式為空則不需要進(jìn)行括號(hào)成對(duì)判斷
        if (null == expression || expression.length() < 1) {
            return true;
        }

        //將傳入的表達(dá)式拆分為char數(shù)組
        char[] expressionsChars = expression.toCharArray();

        //將所有需要判斷的括號(hào)拆分為char數(shù)組并進(jìn)行sort,其中index為偶數(shù)的永遠(yuǎn)是左半個(gè)括號(hào),index為奇數(shù)的是右半個(gè)括號(hào)
        char[] brackets = BRACKET.toCharArray();
        //使用binarySearch之前,需要sort數(shù)組
        Arrays.sort(brackets);

        //用于保存棧容器和括號(hào)之間的對(duì)應(yīng)關(guān)系
        Map<Character, Stack> map = new HashMap<>();

        //遍歷表達(dá)式的每個(gè)字符
        for (char c : expressionsChars) {
            //判斷該字符是否為括號(hào)
            int index = Arrays.binarySearch(brackets, c);
            //負(fù)數(shù),不是括號(hào),不需要處理
            if (index < 0) {
                continue;
            }
            //偶數(shù),是左括號(hào),則放入棧中
            if (index % 2 == 0) {
                //取出map中該左括號(hào)對(duì)應(yīng)的棧容器
                Stack<Character> stack = map.get(c);
                //如果該左括號(hào)對(duì)應(yīng)的key是第一次存入map,則創(chuàng)建一個(gè)棧容器
                if (null == stack) {
                    stack = new IterableResizingArrayStack<>();
                }
                stack.push(c);
                map.put(c, stack);
            } else {
                //奇數(shù),是右括號(hào),則先找到該右括號(hào)對(duì)應(yīng)的左括號(hào)
                char left = brackets[index - 1];
                //取出左括號(hào)對(duì)應(yīng)的棧容器中的值
                Stack<Character> stack = map.get(left);
                //如果該右括號(hào)沒有對(duì)應(yīng)的左括號(hào)與之匹配,則表示此表達(dá)式中的括號(hào)不成對(duì),不合法
                if (null == stack || stack.size() < 1) {
                    return false;
                } else {
                    stack.pop();
                    continue;
                }

            }

        }

        //如果map中還有左側(cè)括號(hào),表示左側(cè)括號(hào)比右側(cè)多,則返回false
        for (char k : map.keySet()) {
            if(!map.get(k).isEmpty()){
                return false;
            }
        }

        return true;
    }
}

經(jīng)過單元測(cè)試驗(yàn)證,該檢查器滿足要求。

雙棧算術(shù)表達(dá)式求值算法

Dijkstra的雙棧算術(shù)表達(dá)式求值算法(Dijkstra's two-stack algorithm for expression evaluation)是由E.W.Dijkstra在上個(gè)世紀(jì)60年代發(fā)明的一個(gè)很簡單的算法,用兩個(gè)棧:一個(gè)用來保存運(yùn)算符、一個(gè)用來保存操作數(shù),來完成對(duì)一個(gè)表達(dá)式的運(yùn)算。

其實(shí)整個(gè)算法思路很簡單:

  • 無視左括號(hào)
  • 將操作數(shù)壓入操作數(shù)棧
  • 將運(yùn)算符壓入運(yùn)算符棧
  • 在遇到右括號(hào)的時(shí)候,從運(yùn)算符棧中彈出一個(gè)運(yùn)算符,再從操作數(shù)棧中彈出所需的操作數(shù),并且將運(yùn)算結(jié)果壓入操作數(shù)棧中
package net.ijiangtao.tech.algorithms.algorithmall.algorithm.stack.evaluation;

import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.Stack;
import net.ijiangtao.tech.algorithms.algorithmall.datastructure.stack.impl.IterableResizingArrayStack;

public class DijkstrasTwoStackAlgorithmForExpressionEvaluation {

    public Double cal(String expression) {

        String[] expressionArr = expression.split(" ");

        Stack<String> ops = new IterableResizingArrayStack<String>();
        Stack<Double> vals = new IterableResizingArrayStack<Double>();

        for (String s : expressionArr) {
            // Read token, push if operator.
            if (s.equals("(")) {
                ;
            } else if (s.equals("+")) {
                ops.push(s);
            } else if (s.equals("-")) {
                ops.push(s);
            } else if (s.equals("*")) {
                ops.push(s);
            } else if (s.equals("/")) {
                ops.push(s);
            } else if (s.equals("sqrt")) {
                ops.push(s);
            } else if (s.equals(")")) {
                // Pop, evaluate, and push result if token is ")"
                String op = ops.pop();
                double v = vals.pop();

                if (op.equals("+")) {
                    v = vals.pop() + v;
                } else if (op.equals("-")) {
                    v = vals.pop() - v;
                } else if (op.equals("*")) {
                    v = vals.pop() * v;
                } else if (op.equals("/")) {
                    v = vals.pop() / v;
                } else if (op.equals("sqrt")) {
                    v = Math.sqrt(v);
                }

                vals.push(v);
            }
            // Token not operator or paren: push double value.
            else {
                vals.push(Double.parseDouble(s));
            }
        }

        return vals.pop();
    }

}

  • 測(cè)試
package net.ijiangtao.tech.algorithms.algorithmall.algorithm.stack.evaluation;

public class DijkstrasTwoStackAlgorithmForExpressionEvaluationTests {

    public  static void main(String[] args){
        DijkstrasTwoStackAlgorithmForExpressionEvaluation expressionEvaluation=new DijkstrasTwoStackAlgorithmForExpressionEvaluation();
        System.out.println(expressionEvaluation.cal("( 1 + ( ( 2 + 3 ) * ( 4 * 5 ) ) )"));
    }

}
  • 測(cè)試輸出:
101.0

links:

author: ijiangtao.net


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容