今天開始,啃讀算法導論第10章。既然是啃就要有啃的樣子,我決定將例題和習題所有用C++實現一遍,總結同一類問題的共性。
10.1節介紹了棧,隊列,雙端隊列及一些組合形式,爲了突出體現思路,讓代碼更加簡潔明瞭,暫且另元素的類型是int,存儲結構都採用定長數組吧。算法
例題1 :棧
下面的代碼就實現了一個簡單的棧,棧內可容納元素個數有上限。在實現每一個類型的時候都應該問問本身,爲何須要維護這些數據成員,多一個會不會更好?少一個可行嗎?數組
由於棧是一種邏輯數據結構,而每種邏輯數據結構的實現都須要依賴一種物理存儲結構的支持,這裏我選擇了數組,因此我須要維護一個數組及其總長度(m_array和m_totalLength)。數據結構
因爲Push、Pop、Top方法中待處理元素的位置信息,不是由參數給定的,而是由棧自身維護的,因此我還須要記錄當前待處理元素(這裏指的是棧頂元素)的下標(m_top),其取值範圍是[-1, m_totalLength-1]。ide
m_top的做用之一是用來計算棧頂元素所對應的數組元素的下標f(m_top),從而將邏輯層操做轉化成存儲層的操做,這裏我將映射法則定義爲f(m_top) = m_top。函數
m_top的做用之二是計算:當前棧內元素個數 = m_top + 1,從而判斷棧是否爲空或已滿。code
這已是數據成員最精簡的版本,一個也不能少。接口
class Stack { public: Stack(int len); //len表示棧的最大長度 ~Stack(); void Push(int val); //壓棧,若棧已滿,報錯」Overflow「 void Pop(); //出棧,若棧爲空,報錯」Underflow「 int Top() const; //讀取棧頂,若棧爲空,報錯」Empty「 bool IsEmpty() const; //棧是否爲空 bool IsFull() const; //棧是否已滿 private: int* m_array; //數組 const int m_totalLength; //棧的最大長度,也是數組的總長度 int m_top; //棧頂元素下標 }; Stack::Stack(int len) :m_totalLength(len), m_array(new int[len]), m_top(-1) {} Stack::~Stack() { delete[] m_array; } void Stack::Push(int val) { if(IsFull()) cerr << "Overflow" << endl; else m_array[++m_top] = val; } void Stack::Pop() { if(IsEmpty()) cerr << "Underflow" << endl; else --m_top; } int Stack::Top() const { if(IsEmpty()) { cerr << "Empty" << endl; return -1; } else return m_array[m_top]; } bool Stack::IsEmpty() const { return m_top == -1; } bool Stack::IsFull() const { return m_top == m_totalLength - 1; }
例題2:隊列
一樣的,隊列可同時容納元素個數有上限。我都須要保存哪些數據成員呢?隊列
數組及其總長度(m_array, m_totalLength)
入隊,出隊方法中待處理元素位置信息,這裏指的是隊頭和隊尾元素的下標,一樣須要由方法內部維護(m_begin, m_end)。element
m_begin, m_end的做用之一是計算隊頭、隊尾所對應數組元素下標,依據邏輯含義應是隻增不減的,而依據環形隊列的映射法則計算出的數組元素下標是在[0, m_totalLength)區間內循環取值的。
m_begin, m_end的做用之二是計算當前容納元素個數,做爲判斷操做合法性的邊界條件。
具體實現中,在不影響上述兩做用的前提下,必須對m_begin,m_end的值加以限制。(見方法Dequeue)it
因爲入隊,出隊方法的被調用頻率不一樣且無關,必須被記錄至兩個變量中,因此數據成員不能更少了。
class Queue { public: Queue(int len); ~Queue(); void Enqueue(int val); void Dequeue(); int Front() const; int Back() const; bool IsEmpty() const; bool IsFull() const; private: int IndexInArray(indexInQueue) const; private: int* m_array; const int m_totalLength; int m_begin; //index of front element int m_end; //index of the one next to back element }; Queue::Queue(int len) : m_totalLength(len), m_array(new int[len]), m_begin(0), m_end(0) { } Queue::~Queue() { delete[] m_array; } void Queue::Enqueue(int val) { if(IsFull()) cerr << "Overflow" << endl; else { m_array[IndexInArray(m_end)] = val; ++m_end; } } void Queue::Dequeue() { if(IsEmpty()) cerr << "Underflow" << endl; else { ++m_begin; //限制m_begin,m_end取值範圍 if(m_begin >= m_totalLength) { m_begin -= m_totalLength; m_end -= m_totalLength; } } } int Queue::Front() const { if(IsEmpty()) { cerr << "Empty" << endl; return -1; } return m_array[IndexInArray(m_begin)]; } int Queue::Back() const { if(IsEmpty()) { cerr << "Empty" << endl; return -1; } return m_array[IndexInArray(m_end - 1)]; } bool Queue::IsEmpty() const { return m_begin == m_end; } bool Queue::IsFull() const { return m_end - m_begin == m_totalLength; } int Queue::IndexInArray(indexInQueue) const { assert(indexInQueue >= 0); return indexInQueue % m_totalLength; }
習題10.1-2 用一個數組存儲兩個棧,只有當兩個棧總長度達到數組總長度時,纔算做棧已滿。
咱們把數組視爲環形數組。
由於須要把一個數組分給兩個棧使用,因此咱們須要設置一個分界線,兩個棧分別以分界線處兩個相鄰元素爲起點,分別向左右兩個方向生長,直到兩者總長度達到數組總長度爲止。
如此說來,除了數組和總長度外,還須要保存一個常量(分界線的位置m_divide)和兩個變量(兩個棧頂元素下標m_top[A],m_top[B])。
m_top[A]和m_top[B]保存的是邏輯層的棧內下標,它的做用和Stack::m_top以及Queue::m_begin,Queue::m_end都是同樣的,一是計算對應的數組元素下標,二是計算邊界條件,判斷調用合法性。
class DoubleStack { public: enum StackID //用A,B標識兩個棧 { A = 0, B = 1 }; DoubleStack(int len); ~DoubleStack(); void Push(StackID id, int val); void Pop(StackID id); int Top(StackID id) const; bool IsEmpty(StackID id) const; bool IsFull() const; //兩個棧會同時到達滿棧條件,因此這裏無需傳入StackID private: int TopIndexInArray(StackID id) const; private: int* m_array; const int m_totalLength; int m_top[2]; const int m_divide; }; DoubleStack::DoubleStack(int len) : m_array(new int[len]), m_totalLength(len), m_divide(len/2) //any value within [0, m_totalLength) is ok { m_top[A] = -1; m_top[B] = -1; } DoubleStack::~DoubleStack() { delete[] m_array; } void DoubleStack::Push(StackID id, int val) { if(IsFull()) { cout << "Overflow" << endl; return; } ++ m_top[id]; m_array[TopIndexInArray(id)] = val; } void DoubleStack::Pop(StackID id) { if(IsEmpty(id)) { cerr << "Underflow" << endl; return; } -- m_top[id]; } int DoubleStack::Top(StackID id) const { if(IsEmpty(id)) { cerr << "Empty" << endl; return -1; } return m_array[TopIndexInArray(id)]; } bool DoubleStack::IsEmpty(StackID id) const { return m_top[id] == -1; } bool DoubleStack::IsFull() const { return m_top[A] + m_top[B] + 2 == m_totalLength; } int DoubleStack::TopIndexInArray(DoubleStack::StackID id) const { //let m_array[m_divide] belongs to stack B if(id == A) return (m_divide - (m_top[A] + 1) + m_totalLength) % m_totalLength; else return (m_divide + m_top[B]) % m_totalLength; }
須要保存哪些信息?和普通線性表不一樣,調用棧的Push,Pop方法時,被操做的元素所處的位置是調用方和被調用方之間的一種約定,這裏約定爲棧頂的元素。相似的,雙方約定調用隊列的Enqueue、Dequeue所操做的元素分別爲隊尾和隊頭。既然這一信息不是由參數傳入,就須要類型自行維護。總結一下,須要保存的是待操做元素的位置信息。每一個棧有一個,每一個隊列有兩個。
邏輯層信息和存儲層信息,選擇保存哪個?假設你有一個菜譜,若是你想照着它炒出一盤菜,你還須要存放和操做食材的廚房。每一個邏輯數據結構就好像一個菜譜,若是你想用程序實現它並運行起來,你還須要一個存儲和操做它的介質,那就是物理存儲結構,好比數組。數組是存儲層的結構,而棧是邏輯層的結構,隨之而產生的是每一個元素都有兩個位置信息,我叫它們邏輯層下標和存儲層下標。二者構成一對映射,一般是滿射。經過映射法則,二者能夠互相求得。因此咱們只須要在數據成員中保存一方,就能夠在須要時計算出另外一方。我在上面的實現中,都選擇了保存邏輯下標,並將計算存儲下標的工做封裝在一個函數中,這樣作的好處是:1. 幾乎每一個接口都有邏輯層的處理,但不是每一個都須要動用存儲層邏輯,因此保存邏輯層信息可使得接口在邏輯層的處理更直接高效。例如,IsEmpty接口就無需計算存儲層下標;2. 當你想改換一種物理存儲結構時,例如從數組改成鏈表,你只須要修改從邏輯層下標到物理層下標的映射過程,靈活性較好。
映射法則能夠很靈活。一般,考慮效率和易讀性,棧下標x到數組下標f(x)的映射法則每每定義爲:f(x) = x。若是你很任性,就想玩些花樣,其實你徹底能夠把數組當作環形數組,將數組的任意位置做爲起始點,好比f(x) = x + 2,就是用數組的第三個元素存儲第一個入棧的元素,滿棧前最後兩個入棧的元素放在數組第一個,第二個元素上。或者你還能夠倒過來存儲,f(x) = 數組總長度 - 1 - x;甚至你能夠毫無規律地將邏輯下標{0, 1, 2, 3}映射成存儲下標{3, 1, 2, 0},只要它是滿射,只要你開心。爲何我要這樣折騰這個映射法則呢?由於有時候,它能夠幫助咱們靈活地解決問題。例以下面的習題10.1-2,如何將兩個棧的邏輯下標映射到一個數組的存儲下標上去,既要彼此不干擾,又能夠最大限度利用數組,這即是映射法則的用武之地了。具體實現見函數DoubleStack::TopIndexInArray。
10.1-4 同例題2
10.1-5 雙端隊列
按照前面總結的規律,須要保存的是待操做元素的位置信息,這裏有兩個待操做元素的位置會移動,因此保存它們在邏輯層的下標,m_begin, m_end,分別表示隊頭和隊尾元素的下一個元素的下標。
class Deque { public: Deque(int len); ~Deque(); void Push_back(int ); void Push_front(int ); void Pop_back(); void Pop_front(); int Front() const; int Back() const; bool IsEmpty() const; bool IsFull() const; private: int IndexInArray(int indexInDeque) const; private: int* m_array; const int m_totalLength; int m_begin; //the index of front element int m_end; //the index of one after the back element }; Deque::Deque(int len) : m_totalLength(len), m_array(new int[len]), m_begin(len/2),//as long as m_begin = m_end,any value > 0 is ok m_end(len/2) {} Deque::~Deque() { delete[] m_array; } void Deque::Push_back(int val) { if(IsFull()) { cerr << "Overflow" << endl; return; } m_array[IndexInArray(m_end)] = val; //set an upper limit to m_end if(++m_end > 2 * m_totalLength) { m_end -= m_totalLength; m_begin -= m_totalLength; } } void Deque::Push_front(int val) { if(IsFull()) { cerr << "Overflow" << endl; return; } //set a lower limit to m_begin if(--m_begin < 0) { m_begin += m_totalLength; m_end += m_totalLength; } m_array[IndexInArray(m_begin)] = val; } void Deque::Pop_back() { if(IsEmpty()) { cerr << "Underflow" << endl; return; } --m_end; } void Deque::Pop_front() { if(IsEmpty()) { cerr << "Underflow" << endl; return; } ++m_begin; } int Deque::Front() const { if(IsEmpty()) { cerr << "Empty" << endl; return -1; } return m_array[IndexInArray(m_begin)]; } int Deque::Back() const { if(IsEmpty()) { cerr << "Empty" << endl; return -1; } return m_array[IndexInArray(m_end-1)]; } bool Deque::IsEmpty() const { return m_begin == m_end; } bool Deque::IsFull() const { return m_end - m_begin == m_totalLength; } int Deque::IndexInArray(int indexInDeque) const { return indexInDeque % m_totalLength; }