想清楚映射規則,棧、隊列、雙端隊列的實現都差很少

今天開始,啃讀算法導論第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;
}
  • 以上三個類型的實現有一些共同的邏輯,是時候來總結一下了。
  1. 須要保存哪些信息?和普通線性表不一樣,調用棧的Push,Pop方法時,被操做的元素所處的位置是調用方和被調用方之間的一種約定,這裏約定爲棧頂的元素。相似的,雙方約定調用隊列的Enqueue、Dequeue所操做的元素分別爲隊尾和隊頭。既然這一信息不是由參數傳入,就須要類型自行維護。總結一下,須要保存的是待操做元素的位置信息。每一個棧有一個,每一個隊列有兩個。

  2. 邏輯層信息和存儲層信息,選擇保存哪個?假設你有一個菜譜,若是你想照着它炒出一盤菜,你還須要存放和操做食材的廚房。每一個邏輯數據結構就好像一個菜譜,若是你想用程序實現它並運行起來,你還須要一個存儲和操做它的介質,那就是物理存儲結構,好比數組。數組是存儲層的結構,而棧是邏輯層的結構,隨之而產生的是每一個元素都有兩個位置信息,我叫它們邏輯層下標和存儲層下標。二者構成一對映射,一般是滿射。經過映射法則,二者能夠互相求得。因此咱們只須要在數據成員中保存一方,就能夠在須要時計算出另外一方。我在上面的實現中,都選擇了保存邏輯下標,並將計算存儲下標的工做封裝在一個函數中,這樣作的好處是:1. 幾乎每一個接口都有邏輯層的處理,但不是每一個都須要動用存儲層邏輯,因此保存邏輯層信息可使得接口在邏輯層的處理更直接高效。例如,IsEmpty接口就無需計算存儲層下標;2. 當你想改換一種物理存儲結構時,例如從數組改成鏈表,你只須要修改從邏輯層下標到物理層下標的映射過程,靈活性較好。

  3. 映射法則能夠很靈活。一般,考慮效率和易讀性,棧下標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;
}
相關文章
相關標籤/搜索