【從今天開始好好學數據結構02】棧與隊列


我們今天主要來談談“棧”以及隊列這兩種數據結構。

回顧一下上一章中【數據結構01】數組中,在數組中只要知道數據的下標,便可通過順序搜索很快查詢到數據,可以根據下標不同自由查找,然而今天要講的“棧”以及隊列這兩種數據結構訪問是受限制的,只允許在一端讀取、插入和刪除數據,這時候對它存在的意義產生了很大的疑惑。因為會覺得,相比數組和鏈表,棧帶給我的只有限制,並沒有任何優勢。那我直接使用數組或者鏈表不就好了嗎?為什么還要用這個“操作受限”的“棧”呢?事實上,從功能上來說,數組或鏈表確實可以替代棧,但你要知道,特定的數據結構是對特定場景的抽象,而且,數組或鏈表暴露了太多的操作接口,操作上的確靈活自由,但使用時就比較不可控,自然也就更容易出錯。

@

1、理解棧與隊列

首先,如何理解“棧”?用現實一個通俗貼切的例子,我們平時放盤子的時候,都是從下往上一個一個放;取的時候,我們是從上往下一個一個地依次取,不能從中間任意抽出,先進后出,這就是典型的“”結構,當某個數據集合只涉及在一端插入和刪除數據,並且滿足后進先出、先進后出的特性,我們就應該首選“棧”這種數據結構。

其次如何理解隊列?同樣用現實一個通俗貼切的例子,平時在校的時候飯堂吃飯都是排隊,而且不能插隊,先進先出,這就是典型的隊列結構

2、用代碼談談棧

實際上,棧既可以用數組來實現,也可以用鏈表來實現。用數組實現的棧,我們叫作順序棧,用鏈表實現的棧,我們叫作鏈式棧。不管是順序棧還是鏈式棧,我們存儲數據只需要一個大小為n的數組就夠了。在入棧和出棧過程中,只需要一兩個臨時變量存儲空間,所以空間復雜度是O(1)。

注意,這里存儲數據需要一個大小為n的數組,並不是說空間復雜度就是O(n)。因為,這n個空間是必須的,無法省掉。所以我們說空間復雜度的時候,是指除了原本的數據存儲空間外,算法運行還需要額外的存儲空間。

空間復雜度分析是不是很簡單?時間復雜度也不難。不管是順序棧還是鏈式棧,入棧、出棧只涉及棧頂個別數據的操作,所以時間復雜度都是O(1)。

還有一點,JVM內存管理中有個“堆棧”的概念。棧內存用來存儲局部變量和方法調用,堆內存用來存儲Java中的對象。那JVM里面的“棧”跟我們這里說的“棧”是不是一回事呢?如果不是,那它為什么又叫作“棧”呢?知道的大牛請自覺評論區見面~在這里插入圖片描述

2.1、用數組實現的棧:順序棧

public class MyStack {
	
	//棧的底層我們使用數組來存儲數據
	int[] elements;

	public MyStack() {
		elements = new int[0];
	}
	
	//壓入元素
	public void push(int element) {
		// 創建一個新的數組
		int[] newArr = new int[elements.length + 1];
		// 把原數組中的元素復制到新數組中
		for (int i = 0; i < elements.length; i++) {
			newArr[i] = elements[i];
		}
		// 把添加的元素放入新數組中
		newArr[elements.length] = element;
		// 使用新數組替換舊數組
		elements = newArr;
	}
	
	//取出棧頂元素
	public int pop() {
		//棧中沒有元素
		if(elements.length==0) {
			throw new RuntimeException("stack is empty");
		}
		//取出數組的最后一個元素
		int element = elements[elements.length-1];
		//創建一個新的數組
		int[] newArr = new int[elements.length-1];
		//原數組中除了最后一個元素的其它元素都放入新的數組中
		for(int i=0;i<elements.length-1;i++) {
			newArr[i]=elements[i];
		}
		//替換數組
		elements=newArr;
		//返回棧頂元素
		return element;
	}
	
	//查看棧頂元素
	public int peek() {
		//棧中沒有元素
		if(elements.length==0) {
			throw new RuntimeException("stack is empty");
		}
		return elements[elements.length-1];
	}
	
	//判斷棧是否為空
	public boolean isEmpty() {
		return elements.length==0;
	}
	
}

2.2、測試數組實現的棧

import demo2.MyStack;

public class TestMyStack {

	public static void main(String[] args) {
		//創建一個棧
		MyStack ms = new MyStack();
		//壓入數組
		ms.push(9);
		ms.push(8);
		ms.push(7);
		//最出棧頂元素
		System.out.println(ms.pop());
		System.out.println(ms.pop());
		System.out.println(ms.pop());
		//查看棧頂元素
//		System.out.println(ms.peek());
		System.out.println(ms.isEmpty());
	}

}

2.3、基於鏈表實現的棧:鏈式棧

package stack;
/**
 * 基於鏈表實現的棧。
 */
public class StackBasedOnLinkedList {
  private Node top = null;

  public void push(int value) {
    Node newNode = new Node(value, null);
    // 判斷是否棧空
    if (top == null) {
      top = newNode;
    } else {
      newNode.next = top;
      top = newNode;
    }
  }

  /**
   * 我用-1表示棧中沒有數據。
   */
  public int pop() {
    if (top == null) return -1;
    int value = top.data;
    top = top.next;
    return value;
  }

  public void printAll() {
    Node p = top;
    while (p != null) {
      System.out.print(p.data + " ");
      p = p.next;
    }
    System.out.println();
  }

  private static class Node {
    private int data;
    private Node next;

    public Node(int data, Node next) {
      this.data = data;
      this.next = next;
    }

    public int getData() {
      return data;
    }
  }
}

3、用代碼談談隊列

棧只支持兩個基本操作:入棧push()和出棧pop()。隊列跟棧非常相似,支持的操作也很有限,最基本的操作也是兩個:入隊enqueue(),放一個數據到隊列尾部;出隊dequeue(),從隊列頭部取一個元素。所以,隊列跟棧一樣,也是一種操作受限的線性表數據結構。隊列的概念很好理解,基本操作也很容易掌握。作為一種非常基礎的數據結構,隊列的應用也非常廣泛,特別是一些具有某些額外特性的隊列,比如循環隊列、阻塞隊列、並發隊列。它們在很多偏底層系統、框架、中間件的開發中,起着關鍵性的作用。比如高性能隊列Disruptor、Linux環形緩存,都用到了循環並發隊列;Java concurrent並發包利用ArrayBlockingQueue來實現公平鎖等。

跟棧一樣,隊列可以用數組來實現,也可以用鏈表來實現。用數組實現的棧叫作順序棧,用鏈表實現的棧叫作鏈式棧。同樣,用數組實現的隊列叫作順序隊列,用鏈表實現的隊列叫作鏈式隊列

3.1、數組實現隊列:順序隊列

package queue;

// 用數組實現的隊列
public class ArrayQueue {
  // 數組:items,數組大小:n
  private String[] items;
  private int n = 0;
  // head表示隊頭下標,tail表示隊尾下標
  private int head = 0;
  private int tail = 0;

  // 申請一個大小為capacity的數組
  public ArrayQueue(int capacity) {
    items = new String[capacity];
    n = capacity;
  }

  // 入隊
  public boolean enqueue(String item) {
    // 如果tail == n 表示隊列已經滿了
    if (tail == n) return false;
    items[tail] = item;
    ++tail;
    return true;
  }

  // 出隊
  public String dequeue() {
    // 如果head == tail 表示隊列為空
    if (head == tail) return null;
    // 為了讓其他語言的同學看的更加明確,把--操作放到單獨一行來寫了
    String ret = items[head];
    ++head;
    return ret;
  }

  public void printAll() {
    for (int i = head; i < tail; ++i) {
      System.out.print(items[i] + " ");
    }
    System.out.println();
  }
}

3.2、鏈表實現的隊列:鏈式隊列

package queue;

/**
 * 基於鏈表實現的隊列
 */
public class QueueBasedOnLinkedList {

  // 隊列的隊首和隊尾
  private Node head = null;
  private Node tail = null;

  // 入隊
  public void enqueue(String value) {
    if (tail == null) {
      Node newNode = new Node(value, null);
      head = newNode;
      tail = newNode;
    } else {
      tail.next = new Node(value, null);
      tail = tail.next;
    }
  }

  // 出隊
  public String dequeue() {
    if (head == null) return null;

    String value = head.data;
    head = head.next;
    if (head == null) {
      tail = null;
    }
    return value;
  }

  public void printAll() {
    Node p = head;
    while (p != null) {
      System.out.print(p.data + " ");
      p = p.next;
    }
    System.out.println();
  }

  private static class Node {
    private String data;
    private Node next;

    public Node(String data, Node next) {
      this.data = data;
      this.next = next;
    }

    public String getData() {
      return data;
    }
  }

}

3.2、循環隊列

用數組來實現隊列的時候會有數據搬移操作,這樣入隊操作性能就會受到影響。那有沒有辦法能夠避免數據搬移呢?我們來看看循環隊列的解決思路。

循環隊列,顧名思義,它長得像一個環。原本數組是有頭有尾的,是一條直線。現在我們把首尾相連,扳成了一個環。

package queue;
public class CircularQueue {
  // 數組:items,數組大小:n
  private String[] items;
  private int n = 0;
  // head表示隊頭下標,tail表示隊尾下標
  private int head = 0;
  private int tail = 0;

  // 申請一個大小為capacity的數組
  public CircularQueue(int capacity) {
    items = new String[capacity];
    n = capacity;
  }

  // 入隊
  public boolean enqueue(String item) {
    // 隊列滿了
    if ((tail + 1) % n == head) return false;
    items[tail] = item;
    tail = (tail + 1) % n;
    return true;
  }

  // 出隊
  public String dequeue() {
    // 如果head == tail 表示隊列為空
    if (head == tail) return null;
    String ret = items[head];
    head = (head + 1) % n;
    return ret;
  }

  public void printAll() {
    if (0 == n) return;
    for (int i = head; i % n != tail; ++i) {
      System.out.print(items[i] + " ");
    }
    System.out.println();
  }
}

好了,到這里,總結一下隊列,隊列最大的特點就是先進先出,主要的兩個操作是入隊和出隊。跟棧一樣,它既可以用數組來實現,也可以用鏈表來實現。用數組實現的叫順序隊列,用鏈表實現的叫鏈式隊列。特別是長得像一個環的循環隊列。在數組實現隊列的時候,會有數據搬移操作,要想解決數據搬移的問題,我們就需要像環一樣的循環隊列。

如果本文章對你有幫助,哪怕是一點點,請點個贊唄,謝謝~

歡迎各位關注我的公眾號,一起探討技術,向往技術,追求技術...說好了來了就是盆友喔...

在這里插入圖片描述


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM