前言
本篇博文想要介紹的是三種功能單一且最爲基礎的數據結構:包,棧,隊列。相信閱讀此篇博客的大部分讀者都能夠手撕或者直接口述出這三種數據結構的實現方法。本篇博文將會從一般實現方式入手,通過分析優劣點來進一步優化三種數據結構的實現方法。
包,棧,隊列的簡介
正如前言中所說。包,棧,隊列是功能單一的數據結構。
包:一種只能塞值不能取值的結構。即這個類對外暴露的api永遠只有add(),沒有get()。他的特性在於遍歷內部數據時,並不一定需要按照順序。側重點在於統計(如求數字的總和,平均值,方差等),而不在於搜索。
舉例:你往一個袋子裏面放不同重量的黃金。你的最終目的只想要知道最後你擁有多少金子,但是並不總是想着要把黃金按照一定的規律拿出來用。
棧:典型的LIFO(last in first out 後入先出)結構。最後一個add()進去的,在調用pop()方法時,能夠第一個獲取到。
舉例:像是你自己的郵箱。你最後收到的信件,往往是最重要且是被你第一個打開的。
隊列:典型的FIFO(first in first out 先入先出)結構。哪個數據先進去,哪個數據先出來。這種現象或者說是流程是我們生活中最普遍的,因爲它能夠體現公平性
舉例:像是在超市排隊付款的隊伍,誰先排的隊伍,誰就能先結賬。
包,棧,隊列的詳細分析
包
SimpleBag
簡單的實現思路如下:
1、使用數組作爲內部存儲容器。每次調用構造方法的時候傳入一個size,用於初始化內部數組;
2、根據包的性質設計公用的api:add(),size(),list();
3、內部維護一個數組的下標,每次add的時候就+1。
public class Bag {
/**
* 內部的數據容器
*/
private int[] data;
/**
* data數組的下標 可以初始化爲0 也可以初始化爲-1 這裏初始化爲-1舉例
*/
private int size = -1;
/**
* 初始化bag
*
* @param maxSize 最大容量
*/
Bag(int maxSize) {
this.data = new int[maxSize];
}
/**
* 往bag裏面添加對象
*/
public void add(int item) {
//每次添加對象都應該判斷一下是否滿了 避免造成數組越界異常
if (!isFull()) {
this.size++;
this.data[this.size] = item;
} else {
throw new RuntimeException("當前bag已滿");
}
}
/**
* 當前的bag是否已經滿了
*/
private boolean isFull() {
return this.size == this.data.length - 1;
}
/**
* 查看目前bag裏面有多少對象了
*/
public int size() {
return this.size;
}
/**
* 展示當前bag裏面擁有的數據
*/
public void list() {
for (int dataItem : this.data) {
System.out.print(dataItem + " ");
}
}
public static void main(String[] args) {
//測試用例
Bag bag = new Bag(3);
bag.add(1);
bag.add(2);
bag.add(3);
System.out.println(bag.size());
bag.list();
bag.add(4);
}
上述實現的不足
- 沒有考慮數據結構的通用性,如果需要的不是int類型的bag,則需要重新寫一份新的bag類。比如StringBag,FruitBag。。。。。。
- 接着第一點講,如果bag裏面存入的是引用類型的數據,那麼list()方法作爲bag本身提供的api就顯得毫無用處。數據結構使用方往往需要在外部獲取到每一個對象並對它們做一些邏輯處理;
- 對於一個數據結構來說,我們大多數情況並不希望它默認是定容的,我們希望它在我們無限制add()後能夠自我增長maxSize,而不是告訴我它滿了。
BetterBag
- 我們需要引入泛型;
- 我們需要支持迭代,簡單理解來說,像java提供的Collection一樣,需要我們自己實現的數據結構支持foreach語法糖;
- 內部容器需要自動伸縮
public class BetterBag<T> implements Iterable<T> {
/**
* 內部的數據容器
*/
private T[] data;
/**
* data數組的下標 可以初始化爲0 也可以初始化爲-1 這裏初始化爲0舉例
*/
private int size = -1;
BetterBag(int initialSize) {
//因爲java不支持創建泛型數組 所以這麼操作
this.data = (T[]) new Object[initialSize];
}
/**
* 往bag裏面添加對象
*/
public void add(T item) {
if (isFull()) {
//如果袋子裝滿了則把容量調整成兩倍
adjustBagSize();
}
this.size++;
this.data[this.size] = item;
}
/**
* 查看目前bag裏面有多少對象了
*/
public int size() {
return this.size;
}
/**
* 當前的bag是否已經滿了
*/
private boolean isFull() {
return this.size == this.data.length - 1;
}
/**
* 調整當前bag的容量 調整爲兩倍大小
*/
private void adjustBagSize() {
T[] newData = (T[]) new Object[this.data.length * 2];
System.arraycopy(this.data, 0, newData, 0, this.data.length);
this.data = newData;
}
@Override
public Iterator<T> iterator() {
return new BagIterator();
}
private class BagIterator implements Iterator<T> {
/**
* 初始化迭代時 數組的下標 因爲是迭代數組 所以肯定是從0迭代到size
*/
private int first = 0;
@Override
public boolean hasNext() {
return this.first <= size;
}
@Override
public T next() {
T tempData = data[this.first];
this.first++;
return tempData;
}
}
/**
* 用於測試泛型的自定義類
*/
private static class Fruit {
public Fruit(String name, Double price) {
this.name = name;
this.price = price;
}
String name;
Double price;
}
public static void main(String[] args) {
BetterBag<Fruit> fruitBag = new BetterBag<>(3);
Fruit apple = new Fruit("蘋果", 5.1);
Fruit grape = new Fruit("葡萄", 6.7);
Fruit orange = new Fruit("橘子", 3.2);
fruitBag.add(apple);
fruitBag.add(grape);
fruitBag.add(grape);
fruitBag.add(orange);
fruitBag.add(orange);
fruitBag.add(orange);
System.out.println(fruitBag.size());
for (Fruit fruit : fruitBag) {
System.out.println(fruit.name + " " + fruit.price);
}
}
}
進一步的思考
上面的bag結構考慮了泛型,迭代,自動伸縮的功能。仔細想想,我們還有哪些地方可以優化的呢?這裏我們提出一個理論,讀者們可以看一下是否認可。
一個好的數據結構對外提供的api,複雜度應該與內部存儲數據量的大小無關。(或者說是影響儘量少)
我們帶着這個論點來回顧一下我們上面的實現。add()方法耗時與數據量大小有明顯的關係。當內部數組數據量足夠大後,一旦觸發adjustBagSize()方法,會導致一個大數組的拷貝。在拷貝過程中內存佔用還特別的大。所以我們接下去就是對add()方法進行優化。
另一種存儲結構——鏈表
對於鏈表我相信我不需要過多的解釋。
優勢 | 劣勢 | |
---|---|---|
數組 | 查詢快 | 添加刪除慢(當數組大小發生變化時) |
鏈表 | 查詢慢 | 在首位添加快 |
對於bag,一個不需要查詢,不需要刪除,只需要存放的數據結構而言。顯然鏈表的存儲方式要優於數組。於是有了我們的最終版代碼
BestBag
public class BestBag<T> implements Iterable<T> {
/**
* 每次添加都在頭部 這樣我們只需要維護一個頭結點就行
* 但是這樣的話 遍歷順序就跟入庫順序相反
*/
private Node first;
private int size;
BestBag() {
}
/**
* 往bag裏面添加對象
*/
public void add(T item) {
Node tempNode = new Node(item);
tempNode.next = first;
first = tempNode;
this.size++;
}
/**
* 查看目前bag裏面有多少對象了
*/
public int size() {
return this.size;
}
@Override
public Iterator<T> iterator() {
return new BestBagIterator();
}
class BestBagIterator implements Iterator<T> {
Node current = first;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public T next() {
T tempItem = current.item;
current = current.next;
return tempItem;
}
}
/**
* 鏈表節點
*/
class Node {
Node(T item) {
this.item = item;
}
private Node next;
private T item;
}
/**
* 用於測試泛型的自定義類
*/
private static class Fruit {
public Fruit(String name, Double price) {
this.name = name;
this.price = price;
}
String name;
Double price;
}
public static void main(String[] args) {
BestBag<BestBag.Fruit> fruitBag = new BestBag<>();
Fruit apple = new Fruit("蘋果", 5.1);
Fruit grape = new Fruit("葡萄", 6.7);
Fruit orange = new Fruit("橘子", 3.2);
fruitBag.add(apple);
fruitBag.add(grape);
fruitBag.add(grape);
fruitBag.add(orange);
fruitBag.add(orange);
fruitBag.add(orange);
System.out.println(fruitBag.size());
for (Fruit fruit : fruitBag) {
System.out.println(fruit.name + " " + fruit.price);
}
}
}
棧
棧的實現也跟包大同小異。差異點在於
- 需要對外提供一個pop方法用於彈出棧的數據,而且要做到LIFO;
Stack
public class Stack<T> implements Iterable<T> {
private Node first;
private int size;
public void push(T value) {
Node tempNode = new Node(value);
tempNode.next = first;
first = tempNode;
size++;
}
private boolean isEmpty() {
return first == null;
}
/**
* 刪除並返回棧頂的第一個元素
*/
public T pop() {
//如果棧空了則返回null
if (isEmpty()) {
System.out.println("棧空了");
return null;
}
T topValue = first.item;
first = first.next;
size--;
return topValue;
}
public int size() {
return this.size;
}
@Override
public Iterator<T> iterator() {
return new StackIterator();
}
class StackIterator implements Iterator<T> {
Node current = first;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public T next() {
T tempItem = current.item;
current = current.next;
return tempItem;
}
}
class Node {
Node(T item) {
this.item = item;
}
Node next;
T item;
}
/**
* 用於測試泛型的自定義類
*/
private static class Fruit {
public Fruit(String name, Double price) {
this.name = name;
this.price = price;
}
String name;
Double price;
}
public static void main(String[] args) {
Stack<Fruit> fruitStack = new Stack<>();
Fruit apple = new Fruit("蘋果", 5.1);
Fruit grape = new Fruit("葡萄", 6.7);
Fruit orange = new Fruit("橘子", 3.2);
fruitStack.push(apple);
fruitStack.push(grape);
fruitStack.push(grape);
fruitStack.push(orange);
fruitStack.push(orange);
fruitStack.push(orange);
int size = fruitStack.size();
System.out.println(size );
for (Fruit fruit : fruitStack) {
System.out.println(fruit.name + " " + fruit.price);
}
System.out.println("開始pop數據");
for (int i = 0; i < size; i++) {
System.out.println(fruitStack.pop().name);
}
//最後一次檢查有沒有空
System.out.println(fruitStack.pop());
}
}
由於我們是不斷的改變頭指針,所以天然就是LIFO。
隊列
隊列與棧的唯一差異就是要FIFO
Queue
package structure.queue;
import java.util.Iterator;
public class Queue<T> implements Iterable<T> {
private Node first;
/**
* 由於需要FIFO 所以我們需要維護last指針,每次入庫都加在鏈表尾部
*/
private Node last;
private int size;
public void enqueue(T value) {
Node tempNode = new Node(value);
if (isEmpty()) {
first = tempNode;
last = tempNode;
} else {
last.next = tempNode;
last = tempNode;
}
size++;
}
private boolean isEmpty() {
return first == null;
}
/**
* 刪除並返回隊列頭部第一個元素
*/
public T dequeue() {
//如果隊列空了則返回null
if (isEmpty()) {
System.out.println("隊列空了");
return null;
}
T topValue = first.item;
first = first.next;
size--;
return topValue;
}
public int size() {
return this.size;
}
@Override
public Iterator<T> iterator() {
return new QueueIterator();
}
class QueueIterator implements Iterator<T> {
Node current = first;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public T next() {
T tempItem = current.item;
current = current.next;
return tempItem;
}
}
class Node {
Node(T item) {
this.item = item;
}
Node next;
T item;
}
/**
* 用於測試泛型的自定義類
*/
private static class Fruit {
public Fruit(String name, Double price) {
this.name = name;
this.price = price;
}
String name;
Double price;
}
public static void main(String[] args) {
Queue<Fruit> fruitQueue = new Queue<>();
Fruit apple = new Fruit("蘋果", 5.1);
Fruit grape = new Fruit("葡萄", 6.7);
Fruit orange = new Fruit("橘子", 3.2);
fruitQueue.enqueue(apple);
fruitQueue.enqueue(grape);
fruitQueue.enqueue(grape);
fruitQueue.enqueue(orange);
fruitQueue.enqueue(orange);
fruitQueue.enqueue(orange);
int size = fruitQueue.size();
System.out.println(size);
for (Fruit fruit : fruitQueue) {
System.out.println(fruit.name + " " + fruit.price);
}
System.out.println("開始dequeue數據");
for (int i = 0; i < size; i++) {
System.out.println(fruitQueue.dequeue().name);
}
//最後一次檢查有沒有空
System.out.println(fruitQueue.dequeue());
}
}
其他要點
- 問:萬一包,棧,隊列需要從某一個指定的位置獲取數據怎麼辦?用鏈表豈不是會很慢?
答:數據結構需要明確職責。我們在設計每一個數據結構的時候要避免出現寬接口的現象。如java自身提供的java.util.Stack,它對外甚至提供了從棧底添加數據的方法。一個好的數據結構,當我們見到它時就知道這一塊我們需要用到它的特性。如在代碼中使用了Stack而不是Queue,我們一下子就能非常直觀的看出這裏LIFO很重要。 - 如果使用數組來實現棧和隊列,有一個要點非常容易被忽略——記得收縮數組。當我們不斷的往棧添加數據時,我們會把數組的大小一直放大。但當我取出大部分數據後,數組後面的那些對象並不會被jvm回收。但是由於我們的指針已經縮小了,我們再也訪問不到那些數據了。這種情況也被稱爲對象的遊離。所以記得在size遠小於data.length的時候,把數組總長度按一定比例收縮。
總結
- 好的數據結構需要支持迭代,泛型,自動伸縮;
- 數據結構要避免寬接口;