在C++中,引入了类的概念,要用类来实现顺序表,一定要注意代码的复用,这样会使实现更加容易。
数组实现顺序表:
定义三个指针,_first指向数组开始位置,_finish指向最后一个元素的下一个位置,_endofstorage指向数组末尾的下一个位置。如图所示,假设数组中有元素1234567:
构造函数:让三个指针指向空;
typedef int DataType;
class Vector
{
public:
Vector()
:_first(NULL)
,_finish(NULL)
,_endofstorage(NULL)
{}
Vector(const Vector& v)
{
if(Size() > 0)
{
size_t size = v.Size();
DataType* tmp = new DataType[size];
memcpy(tmp, v._first, size * sizeof(DataType));
_first = tmp;
_finish = _first + size;
_endofstorage = _first + size;
}
else
{
_first = _finish = _endofstorage = NULL;
}
}
~Vector()
{
delete[] _first;
}
size_t Size()const;
size_t Capacity()const;
//Vector& operator=(const Vector& v);
Vector& operator=(Vector v);
void expand(size_t n);
void print(const char* msg)const;
void PushBack(DataType x);
void PopBack();
void Insert(size_t pos, DataType x);
void Erase(size_t pos);
size_t Find(DataType x);
void Reserve(size_t n);
private:
DataType* _first;
DataType* _finish;
DataType* _endofstorage;
};
数组元素的个数:
由于指针减指针结果为两指针间相差元素的个数,我们可以用_finish-_first来计算数组元素的个数。
size_t Vector::Size()const
{
return _finish - _first;
}
数组容量的大小:
计算方法与数组元素个数类似。
size_t Vector::Capacity()const
{
return _endofstorage - _first;
}
对顺序表进行扩容:
若扩充到可以存放n个数据,
如果n比原有数组容量大,进行扩容,重新申请空间,将原有数组的数据拷贝到新申请的空间上,在释放原来的数组空间,将指针指向新申请的空间;
否则,什么都不做。
void Vector::expand(size_t n)
{
if(n > Capacity())
{
size_t size = Size();
DataType* tmp = new DataType[n];
memcpy(tmp, _first, Size()*sizeof(DataType));
delete[] _first;
_first = tmp;
_finish = _first + size;
_endofstorage = _first + n;
}
return;
}
赋值运算符的重载:
拿v1=v2来说,v1调用了operator=方法,在该方法中为*this,v2就是传进该方法的参数。
如果v2元素个数大于0,进行赋值操作:
1.若v1的容量小于v2的元素个数,说明v1放不下v2的所有元素,要进行扩容;
2.将v2的元素一次拷贝给v1;(可以自己实现,也可以用memcpy函数)
3.重新确定各个指针指向的位置。
否则,直接将_first和_finish置为NULL。
Vector& Vector::operator=(const Vector& v)
{
if(v.Size() > 0)
{
if(v.Size() > Capacity())
expand(v.Size());
memcpy(_first, v._first, v.Size() * sizeof(DataType));
_finish = _first + v.Size();
_endofstorage = _first + v.Size();
}
else
{
_finish = _first;
}
return *this;
}
以上是传统写法。还有一种写法叫现代写法,是传参时直接传vector,这样在调用时,会自动拷贝构造一个与v2数据相同的顺序表,假设为tmp;将*this的各个指针与tmp的交换。这时就赋值成功了。最后在调用完方法后,tmp的生命周期结束,系统会自动调用析构函数。
Vector& Vector::operator=(Vector v)
{
swap(_first, v._first);
swap(_finish, v._finish);
swap(_endofstorage, v._endofstorage);
return *this;
}
往顺序表的某位置插入元素:
首先,检查插入位置是否合法;
其次,判断顺序表容量是否为空,若为空,扩容;
判断顺序表是否满了,若满了,扩容;
再次,移动表中元素,以便新的元素插入合适的位置;
最后,插入元素。
void Vector::Insert(size_t pos, DataType x)
{
assert(pos <= Size());
if(Capacity() == 0)
expand(1);
if(Size() == Capacity())
expand(2*Capacity());
DataType* p = _first + pos;
memmove(p+1, p, (_finish - p)*sizeof(DataType));
*p = x;
_finish++;
return;
}
删除某位置元素:
首先,判断顺序表是否为空,为空则直接返回;不为空,进行下面的操作;
其次,将要删除元素之后的元素向前移动一个位置;
最后,将_finish--。
void Vector::Erase(size_t pos)
{
if(Size() == 0)
return;
assert(pos < Size());
DataType* p = _first + pos;
memmove(p, p+1, (_finish - p - 1)*sizeof(DataType));
_finish--;
return;
}
查找元素位置:
定义一个指针指向第一个元素,遍历每个元素,并判断是否为要查找的元素,若是,则返回该指针-_first得到的值;直到遍历完都没有返回,即为没有找到该元素,返回(size_t)-1.
size_t Vector::Find(DataType x)
{
DataType* cur = _first;
while(cur != _finish)
{
if(*cur == x)
return cur - _first;
cur++;
}
return (size_t)-1;
}
PushBack:
在Size()位置插入元素。
void Vector::PushBack(DataType x)
{
// if(Capacity() == 0)
// expand(1);
// if(Capacity() == Size())
// expand(2*Capacity());
// *_finish++ = x;
Insert(Size(), x);
return;
}
PopBack:
考虑到代码的复用性,可以理解为删除Size()-1位置上的元素。
void Vector::PopBack()
{
// if(Size() == 0)
// return;
// _finish--;
Erase(Size() - 1);
return;
}
带头结点的双向循环链表的实现:
如图,分别是带头结点的含有4个元素的双向循环链表和带头节点的空链表:
先定义结点,在这个中要有数据,指向下一个结点的指针,和指向前一个结点的指针。
typedef int DataType;
struct ListNode
{
ListNode* _next;
ListNode* _prev;
DataType _data;
ListNode(DataType x)
:_next(NULL)
,_prev(NULL)
,_data(x)
{}
};
然后定义链表的类:
构造函数:
由于该链表是一个带头结点的链表,在构造函数中对链表进行初始化时要创建一个头结点,并把指针都指向自己。这样就初始化为了一个双向循环的空链表。
拷贝构造:
这里采用深拷贝的方式进行。先用构造函数中的方法先对链表(假设A)进行初始化,然后遍历要拷贝的链表(假设B),将遍历的元素后插到链表A中。直到遍历完成。
析构函数:
析构函数要对申请的所有空间进行释放,否则会造成内存泄漏。也就是说要释放每个结点,但要注意的是,再将所有节点释放完之前,不能释放头结点。
class List
{
typedef ListNode Node;
public:
List()
:_head(new Node(DataType()))
{
_head->_next = _head;
_head->_prev = _head;
}
List(List& l)
:_head(new Node(DataType()))
{
_head->_next = _head;
_head->_prev = _head;
Node* cur = l._head->_next;
while(cur != l._head)
{
PushBack(cur->_data);
cur = cur->_next;
}
}
void PushBack(DataType x);
void PushFront(DataType x);
void PopBack();
void PopFront();
Node* Find(DataType x);
void Insert(Node* pos, DataType x);
void Erase(Node* pos);
List& operator=(const List& l);
~List()
{
Node* cur = _head->_next;
while(cur != _head)
{
_head->_next = cur->_next;
cur->_next->_prev = _head;
delete cur;
cur = _head->_next;
}
delete _head;
_head = NULL;
}
private:
Node* _head;
};
在某位置之前插入元素:
首先,检查pos是否合法;
其次,在链表中查找pos位置cur,并注意保存pos位置的前一个位置pre;
最后,创建结点并将结点连接到pre和cur之间的位置;
void List::Insert(Node* pos, DataType x)
{
assert(pos);
Node* cur = _head->_next;
Node* pre = _head;
while(cur != _head)
{
if(cur == pos)
{
break;
}
pre = cur;
cur = cur->_next;
}
//注意如果用Node tmp(x);该函数调用完后,会自动调用析构函数,新创建的结点就析构了;此时如果去访问链表的每个节点,
//不但会访问不到还可能会访问内存冲突
Node* tmp = new Node(x);
pre->_next = tmp;
tmp->_prev = pre;
tmp->_next = cur;
cur->_prev = tmp;
return;
}
删除某位置结点:
首先,检查pos是否合法;
其次,检查要删除的结点是否是头结点,如果是,直接返回;
再次,在该链表中查找该节点,并将该节点的前一个结点和后一个结点连接起来;
最后,再释放pos位置的结点。
void List::Erase(Node* pos)
{
assert(pos);
if(pos == _head)
{
return;
}
Node* cur = _head->_next;
Node* pre = _head;
while(cur != _head)
{
if(cur == pos)
{
pre->_next = cur->_next;
cur->_next->_prev = pre;
delete pos;
return;
}
pre = cur;
cur = cur->_next;
}
return;
}
查找某元素位置:
遍历一次链表,同时对比每个节点的值与要查找的元素是否相同,若相同,则返回该节点;否则,继续查找下一个结点。直到把链表遍历完,若还没找到,则返回NULL。
ListNode* List::Find(DataType x)
{
Node* cur = _head->_next;
while(cur != _head)
{
if(cur->_data == x)
return cur;
cur = cur->_next;
}
return NULL;
}
PushBack:
相当于在链表的末尾插入元素,即在头结点的前面插入元素。
void List::PushBack(DataType x)
{
Insert(_head, x);
return;
}
PushFront:
类似的,相当于在头结点的后面插入元素。
void List::PushFront(DataType x)
{
Insert(_head->_next, x);
return;
}
PopBack:
删除头结点的前一个结点。
void List::PopBack()
{
Erase(_head->_prev);
return;
}
PopFront:
删除头结点的下一个结点。
void List::PopFront()
{
Erase(_head->_next);
return;
}
赋值运算符的重载:
首先,要将this指向的链表的结点全部释放(除了头结点);
其次,类似于拷贝构造,将要拷贝的链表的每个元素后插到this指向的链表;
最后,返回*this。
List& List::operator=(const List& l)
{
Node* cur = _head->_next;
Node* pre = _head;
Node* next = cur->_next;
while(cur != _head)
{
pre->_next = next;
next->_prev = pre;
delete cur;
cur = next;
next = next->_next;
}
cur = l._head->_next;
while(cur != l._head)
{
PushBack(cur->_data);
cur = cur->_next;
}
return *this;
}