今天开始,啃读算法导论第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; }