資料結構與演算法(4) -- list、queue以及stack
今天主要給大家介紹幾種資料結構,這幾種資料結構在實現原理上較為類似,我習慣稱之為類list的容器。具體有list、stack以及queue。
list的節點Node
首先介紹下node,也就是組成list的節點。從面向物件的角度來說節點也是就一個類,list裡面包含了node物件的例項,以及操作/管理這些例項的方法。先給出一個粗糙的node的C++程式碼如下程式碼塊所示。可以看出除了保有當前節點的資訊,其還有指向前一個節點和後一個節點的指標。如果單獨使用當然另外還需要相應的建構函式。如果是使用node組成其他的如list資料結構,也可以不需要node自己的建構函式,而完全交由list分配空間和資料,這部分可以參考《STL原始碼剖析》。好吧,扯得有點遠了,為了講述的簡單,後面的程式碼都將採取簡易的實現方式,如有不妥歡迎私信。
template<typename T>
struct Node
{
typedef void * void_pointer;
void_pointer prev;
void_pointer next;
T data;
Node(T data){
this->data = data;
prev = nullptr;
next = nullptr;
}
};
Node構成的資料結構有哪些
說完了node,那麼由node(或其變體)作為基本單元的資料結構有哪些呢?當然首先有list(連結串列),其次stack(棧)以及queue(佇列)也是哈。list是雙向連結的,頭和尾都可以插入和刪除node,當然中間插入刪除也是可以的。而stack則是先進後出的佇列,啥意思呢,也就是說:插入和刪除都是針對最頂層的node的;queue則不同,它是先進先出的,和我們日常排隊買東西是一樣的原理。接下來分別介紹。
list
先來個圖感性認識一下:
其實看了圖之後也就無需多言了哈,不就是node一個連結一個麼。嘿,簡單!!!
stack
那stack呢,更加簡單啦。只要將雙向的連結串列換成單向的連結串列就可以實現了哈。在表的前端插入來實施push操作,通過刪除表的前端元素來完成pop操作。top操作知識考察表前端元素並返回它的值。(當然stack也可以用基於vector來實現)。
queue
先進先出,其實現可以基於連結串列。但是也可以基於deque/vector。與stack一樣若基於已有的結構,其實現是較為簡單的,這裡不詳細敘述。
實現一個list
其實從stl的原始碼可以看出,要真正實現一個list並不像看上去那麼簡單。考慮到記憶體分配以及構造的細節,還是比較麻煩的。這裡我們主要是為了掌握list的基本結構,對其主要操作做一個瞭解。所以不必寫那麼複雜的實現(哈哈,太複雜的我也寫不好,還是建議閱讀原始碼哈)。
list的迭代器
迭代器是後需幾乎所有操作的基礎。那麼怎麼組織這個迭代器呢,或者換句換說,迭代器在哪裡定義呢?我們可以這麼組織我們的程式碼:
using namespace std;
template <typename Object>
class List
{
private:
struct Node
{
...
};
public:
class const_iterator
{
public:
...
protected:
Node *current;
...
friend class List<Object>;
};
class iterator : public const_iterator
{
public:
...
friend class List<Object>;
};
public:
List( )
{ init( ); }
~List( )
//其他方法:
...
...
private:
int theSize;
Node *head;
Node *tail;
void init( )
{
theSize = 0;
head = new Node;
tail = new Node;
head->next = tail;
tail->prev = head;
}
};
稍微分析一下,首先list由node一個連結一個構成,所以需要一個node類。另外不希望第三方類使用,所以是私有的。在看迭代器,迭代器是為了方便遍歷list或者指代list的當前位置的。所以其本質是一個指標,而且是指向node的指標,所以可以看到const_iterator類有個指標的定義:Node *current;。因為有時候通過迭代器只是想單純訪問資料而已,而有時候是希望能夠改變node節點的資料。所以就有了const_iterator以及iterator兩套哈。我不打算在這裡給出迭代器的實現細節,這部分內容有很多教材的原始碼都是不錯的,STL原始碼可能複雜些不建議。推薦《資料結構與演算法分析–C++語言描述(第四版)》,請參考我的GitHub。但是還是有幾個細節需要提一下:
- 迭代器的類要是list的友元。
- 迭代器在某種意義上就是某種智慧指標,所以過載*以及->(或者++, --)操作是必須的。
- 為了後續操作的方便,在list中==以及!=通常也少不了。
- 通常需要head(表示第一個元素的前一個位置)以及tail(最後一個元素的最後一個位置)。這是為了在遍歷的時候可以用迭代器!=head以及tail來判斷是否到達邊界。
幾個list重要操作的原理與實現
現在假設我們已經有了一個功能完整的迭代器,那麼接下來的操作基於迭代器就好了。
刪除
先給出圖和程式碼:
iterator erase( iterator itr )
{
Node *p = itr.current;
iterator retVal( p->next );
p->prev->next = p->next;
p->next->prev = p->prev;
delete p;
--theSize;
return retVal;
}
iterator erase( iterator from, iterator to )
{
for( iterator itr = from; itr != to; )
itr = erase( itr );
return to;
}
直觀上看我們只要刪除1和2號線,增加3和4號線就可以了。程式碼就是上面的程式碼哈,很簡單。但是,經常容易犯得錯誤是:忘記先儲存刪除節點的位置。那麼在1與2號線刪除之後就再也無法釋放其資源了哈,這就是第一行程式碼 Node *p = itr.current; 的用意。第二行程式碼 iterator retVal( p->next ); 是為了刪除之後仍然能夠得到有效的迭代器。
插入
插入操作也是型別的,只不過這裡用了一個較為看上去複雜的操作 p->prev = p->prev->next = new Node{ x, p->prev, p } 。記住,c++賦值語句,是從右往左執行的。結合圖,也就不難理解這句話了。
// Insert x before itr.
iterator insert( iterator itr, const Object & x )
{
Node *p = itr.current;
++theSize;
return iterator( p->prev = p->prev->next = new Node{ x, p->prev, p } );
}
// Insert x before itr.
iterator insert( iterator itr, Object && x )
{
Node *p = itr.current;
++theSize;
return iterator( p->prev = p->prev->next = new Node{ std::move( x ), p->prev, p } );
}
其他–stl/java
stl裡list的實現實際上稍有出入,主要體現在記憶體分配與構造。實在是能力有限,在這裡不敢瞎說。推薦大家閱讀侯捷老師的《STL原始碼剖析》。另外java裡的迭代器需要實現Iterator介面就好了。
關於queue與stack的基於node的實現就不贅述了。另外dequeue也可基於node實現,但是stl裡是基於分段連續線性空間實現的,如果有可能會在後續的部落格中詳細講述。
最後給一個我之前寫的dequeue-java版本作為結束。
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Deque<Item> implements Iterable<Item> {
private Node first = null;
private Node last = null;
private int size = 0;
public Deque() {
}
private class Node{
Item item;
Node next;
Node previous;
}
public boolean isEmpty() {
return first==null && last==null;
}
public int size() {
return size;
}
public void addFirst(Item item) {
if(item==null)
throw new IllegalArgumentException();
if(0==size) {
//if size==0, let first and last point to item
first = new Node();
first.item = item;
last = first;
}else {
Node oldfirst = first;
first = new Node();
first.item = item;
first.next = oldfirst;
oldfirst.previous = first;
}
size++;
}
public void addLast(Item item) {
if(item==null)
throw new IllegalArgumentException();
if(0==size) {
//if size==0, let first and last point to item
last = new Node();
last.item = item;
first = last;
}else {
Node oldlast = last;
last = new Node();
last.item = item;
last.previous = oldlast;
oldlast.next = last;
}
size++;
}
public Item removeFirst() {
if(size==0)
throw new NoSuchElementException();
Item itemtem = first.item;
if(size==1) {
first = null;
last = null;
}else {
Node oldfirst = first;
first = oldfirst.next;
first.previous = null;
oldfirst.next = null;
oldfirst.item = null;
}
size--;
return itemtem;
}
public Item removeLast() {
if(size==0)
throw new NoSuchElementException();
Item itemtem = last.item;
if(size==1) {
itemtem = last.item;
first = null;
last = null;
}else {
Node oldlast = last;
last = oldlast.previous;
last.next = null;
oldlast.previous = null;
oldlast.item = null;
}
size--;
return itemtem;
}
private class DequeIterator implements Iterator<Item>{
private Node current = first;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Item next() {
if(!hasNext())
throw new NoSuchElementException();
Item itemtem = current.item;
current = current.next;
return itemtem;
}
public void remove() {
throw new UnsupportedOperationException();
}
}
public Iterator<Item> iterator(){
return new DequeIterator();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
Deque<Integer> deque = new Deque<Integer>();
deque.addFirst(1);
deque.addFirst(2);
deque.addFirst(3);
deque.addLast(4);
deque.addLast(5);
deque.addLast(6);
deque.removeFirst();
deque.removeLast();
deque.removeFirst();
deque.removeLast();
deque.removeFirst();
deque.removeLast();
for (Integer integer : deque) {
System.out.println(integer);
}
}
}
See you next time. Happy Coding!!!
我的github