数据结构就其字面意义来看,就是数据和结构。我们有一堆数据,它们之间有一定的结构关系,数据就像是砖头,把它们按规律放置就建成了有结构的房子。
我们摆放数据的方式有多种。把数据挨个放,除了第一个数据和最后一个数据外,每个数据的前面和后面都各有一个数据(专业术语就是都有前驱和后继),这是一种一对一的数据结构,就是我们今天要详述的线性表。
举个简单的例子,我是一个面试官,我要面试前来应聘的人,面试开始前已经到了30个人。有一点需要考虑的是这些面试的人在哪里待着呢?
第一种情况是:HR想了今天总共也就39个人,我们这正好有个房间里面有40个编了号的座位,我让他们坐到房间里吧。于是HR让这30个人在房间里按照顺序排好队伍坐下,此时如果第31个人来了,HR知道房间里还有10个座位,于是便让他进了房间,接着又连来了8个人,HR让这8个人入座后,房间里装了39个人,就剩下一个座位,房间的利用率相当高了。
第二种情况是:HR想了今天有估计远不止40个人来面试,公司那个房间也就只能装40人。于是HR告诉1号面试人,排在他后面的2号面试人的姓名,告诉2号面试人排在他后面的三号面试人的姓名,这样每个人都知道了排在他的后面的面试人的姓名。1号面试结束后,大吼一声2号的姓名,2号就知道轮到自己了。如此一来,HR便不用给他们集中在一个房间里让他们按顺序坐下了,他们爱在哪待着就在哪待着,因为他们知道自己的前一位知道他的名字,到时会喊他的。
上述例子中的第一种情况反映的就是线性表的第一个种存储方式:顺序表。就是用一块连续的内存空间来存储数据。数据在顺序表中按照先后顺序存放,可以随机访问,比如HR来到房间,便可直接找到坐在第20号座位上的第20个人。同时我们也可以看出,顺序表中可以装的数据总量是固定。
第二种情况反映的是线性表的另一种存储方式:链表。链表的物理存储结构是地址任意的存储单元,不能随机访问,得从第一个数据依次往后进行访问。但是,链表的容量是不固定。
下面结合cpp详细阐述这两种存储方式。
1.顺序表
cpp是面向对象编程,一切都是对象,顺序表也是个对象,我们想要使用顺序表,很显然只需要那出顺序表这个类,我们不妨把该类定义为SeqList,考虑到要顺序表中装的数据类型可能是char,可能int等等,故才用模板类。这样我们便可在main()函数里如此创建一个容量为10的顺序表对象:
SeqList<int> *seqList1 = new SeqList<int>(10);
接着我们便可以对该顺序表进行插入,删除,修改,获取指定位置元素值等操作,如
seqList1->insertElementToEnd(1);//把1插入到顺序表
seqList1->insertElementToEnd(2);//把2插入到顺序表
seqList1->insertElementToEnd(3);//把3插入到顺序表
seqList1->insertElementToEnd(4);//把4插入到顺序表
seqList1->changeElementAtIndex(2,10);//把索引为2的元素值变为10
seqList1->deleteElementAtIndex(2);//删除索引为2的元素
seqList1->getLength();//获取顺序表的长度
有了以上接口,接下来只需考虑如何实现就行了。考虑到数组采用的便是一段连续的存储空间,可以利用数组来实现顺序表。下面是SeqList类的具体定义:
template<typename DataTpye> class SeqList
{
public:
//构造和析构
SeqList(int capacity);
~SeqList(void)
{
delete _datas;
}
//顺序表的操作
bool insertElementToEnd(DataTpye data);//插入到顺序表的尾部,如果成功,返回true
bool deleteElementAtIndex(int index);//删除指定位置的元素,如果成功,返回true
bool changeElementAtIndex(int index,DataTpye newData);//把指定位置的值修改成newData,如果成功返回true
DataTpye getElementAtIndex(int index);//获取指定位置的元素值
int getLength();//获取顺序表的长度
void printAllElement();//打印顺序表中的元素
private:
DataTpye* _datas;//实际用来存放元素的数组
int _capacity;//线性表的最大容量
int _length;//线性表的长度
};
完整的实现代码是:
template<typename DataTpye> class SeqList
{
public:
//构造和析构
SeqList(int capacity)
{
_capacity = capacity;
_length = 0;
_datas = new DataTpye[capacity];
for (int i =0;i<capacity;i++)
{
_datas[i] = NULL;
}
}
~SeqList(void)
{
delete _datas;
}
//顺序表的操作
bool insertElementToEnd(DataTpye data)//插入到顺序表的尾部,如果成功,返回true
{
if (_length >= _capacity)return false;
_datas[_length++] = data;
return true;
}
bool deleteElementAtIndex(int index)//删除指定位置的元素,如果成功,返回true
{
if (index > _length || index <= 0)return false;
for (int i = index;i<_length;i++)
{
_datas[i-1] = _datas[i];
}
_length--;
return true;
}
bool changeElementAtIndex(int index,DataTpye newData)//把指定位置的值修改成newData,如果成功返回true
{
if (index > _length || index <= 0)return false;
_datas[index-1]=newData;
return true;
}
DataTpye getElementAtIndex(int index)//获取指定位置的元素值
{
if (index > _length || index <= 0)return false;
return _datas[index-1];
}
int getLength(){//获取顺序表的长度
return _length;
}
void printAllElement(){//打印顺序表中的元素
cout<<"元素有"<<endl;
for (int i = 0;i<_length;i++)
{
cout<<_datas[i]<<endl;
}
}
private:
DataTpye* _datas;//实际用来存放元素的数组
int _capacity;//线性表的最大容量
int _length;//线性表的长度
};
2.链表
我们依然从如何在代码中方便地使用链表出发,进而定义出相应的接口。链表依旧是一个类,不妨叫做LinkList,因为用链表实现线性表不需要考虑容量的问题,我们只需new一个LinkList,即可得到一个指向LinkList的指针,
LinkList<int> *linkList = new LinkList<int>();
接着我们只需要操作linkList这个对象即可。具体的接口与上面的顺序表的接口差不多:
linkList->insertElementToEnd(1);//在链表尾部插入一个元素
linkList->insertElementToEnd(2);//在链表尾部插入一个元素
linkList->insertElementToEnd(4);//在链表尾部插入一个元素
linkList->insertElementToEnd(5);//在链表尾部插入一个元素,此时链表中应该是1245
linkList->printAllElement();
linkList->insertElementToIndex(3,3);//在索引3出插入一个元素,此时链表中应该是12345
linkList->printAllElement();
linkList->changeElementAtIndex(5,6);//把索引5处的值修改为6,此时链表中应该是6
有了接口,下面就是具体的实现了。由于每个元素都要记录它的下一个元素,故每个元素都应该有一个指针,这样我们就封装一个节点(Node)类,该类有两个成员变量_data和_nextNode。
template<typename DataType> class LinkNode
{
public:
LinkNode(DataType data)
{
_data = data;
_nextNode = NULL;
}
private:
DataType _data;//数据域
LinkNode* _nextNode;//指针域
friend LinkList<DataType>;
};
有了节点类,我们就可以开始写链表类了,
template<typename DataType> class LinkList
{
public:
LinkList()
{
_head = new LinkNode<DataType>(0);
}
~LinkList()
{
}
//链表的操作
bool insertElementToEnd(DataType data);//插入到链表的尾部,如果成功,返回true
bool insertElementToIndex(int index,DataType data);//插入到链表的指定位置处
bool deleteElementAtIndex(int index);//删除指定位置的元素,如果成功,返回true
bool changeElementAtIndex(int index,DataType newData);//把指定位置的值修改成newData,如果成功返回true
DataType getElementAtIndex(int index);//获取指定位置的元素值
int getLength();//获取链表的长度
void printAllElement();//打印表中的元素
private:
int _length;//用于记录链表的当前长度
LinkNode<DataType> * _head;//链表的头节点
};
是不是和之前的顺序表基本一样呢。只是在链表类中不需要定义容量,但需要定义一个头节点LinkNode<DataType> * _head;
(也可以不定义头节点,但是使用头节点可以简化一些操作,这里我们采用带头节点的结构)。首先来理解插入操作,线性表的插入操作需要把插入位置及其之后的所有元素向后移动,这需要很大的开销。而链表的插入要简单的多,只需要把新节点的指针域_nextNode指向要插入位置的节点,再把要插入位置之前的节点的指针域指向新节点(如下图所示)。反映在代码上就是
template<typename DataType> bool LinkList<DataType>::insertElementToIndex(int index,DataType data)
{
if (index<=0 || index > _length)return false;
LinkNode<DataType> * p = _head;
for (int i = 0;i<index-1;i++)//把指针p移动到索引位置处节点的前一个节点
{
p = p->_nextNode;
}
LinkNode<DataType> *newNode = new LinkNode<DataType>(data);
newNode->_nextNode = p->_nextNode;
p->_nextNode = newNode;
_length++;
return true;
}
把元素插入到链表的末尾就很容易了:
template<typename DataType> bool LinkList<DataType>::insertElementToEnd(DataType data)
{
//先让head指向头指针,如果链表为空,即只有头指针,则head->_nextNode就为NULL
LinkNode<DataType> *head = _head;
while (head->_nextNode)//找到最后一个节点
{
head = head->_nextNode;
}
LinkNode<DataType> *newNode = new LinkNode<DataType>(data);
if (newNode == NULL)return false;
head->_nextNode = newNode;
_length++;
return true;
}
删除一个节点时,只需要把它的前驱节点指向要删除节点的后继节点并释放删除节点的内存即可。
template<typename DataType> bool LinkList<DataType>::deleteElementAtIndex(int index)
{
if (index<=0 || index > _length)return false;
LinkNode<DataType> * p = _head;
for (int i = 0;i<index-1;i++)//把指针p移动到索引位置处节点的前一个节点
{
p = p->_nextNode;
}
LinkNode<DataType> * temp = p->_nextNode;
p->_nextNode = p->_nextNode->_nextNode;
delete temp;
return true;
}
其余的操作就很简单了,就不多述了。
总结一下,线性表反映的是逻辑上的结构,而顺序表和链表是线性表的两种具体实现形式,首先我们要在逻辑上想清楚线性表要有哪些性质和操作,最好定义出自己认为方便使用的接口,接下来去用代码实现起来就很方便了。