线性表
数据类型的概念
我曾经问过我的一些非计算机专业朋友对“数据类型”这个概念怎么理解,他们的回答基本上都是说数据类型应该就是指数据的一种类型划分吧,就像把人分成白种人、黑种人和黄种人,或者把动物分成什么种属之类的。
确实是这样,数据类型就是对数据的一种类型标注,比如最基本的一些数据类型,int型表示32位2进制的整数类型,double表示双精度浮点类型、char表示字符类型。在计算机世界中,所以的信息都是离散的,因为现代计算机只能处理二进制数据,数据必须编码成二进制后计算机才能理解以及处理。在信息储存时,也只能把数据编码成二进制后才能储存在一些物理介质中,现代的储存介质基本都是磁介质或者半导体材料的,都只能储存二进制数据,比如说磁介质,用阴极和阳级分别表示0和1。由于计算机世界只存在0和1两个数字,因而表示其他数据类型的时候就必须有个长度,如果没有长度限制的话,全是0和1的组合就不知道一个数据由哪一段0和1组合构造了,这样混乱的0和1数据也就失去了存在的意义。这是导致数据类型有长度属性的决定性原因。由于长度的问题,就导致了计算机数据必须都是一个一个的,在现实生活中,时间是连续的(就不考虑量子力学了),但在计算机世界中,要表示时间,就必须用一个一个时间点来表示。
计算机世界由于都是0和1,不容易让人理解,所以就引入了抽象数据类型(ADT,Abstract Data Type)在计算机的数据世界中表示真实世界的万事万物,比如说现实世界中的一个人,有年龄、性别、姓名等等,那么就在计算机中也用一个People类型来表示人,这种People数据类型也有年龄,年龄用一个整数来表示,性别可以用一个字符来表示,'w'表示女性,'m'表示男性,姓名可以用一个字符串来表示,也就是一个字符的序列。人在现实世界中的属性不可以被完全描述的,而在用计算机来描述一个人的时候,都是有特定场景的,因而也只需要关注这个场景所需要人的哪些属性。比如说人在学生管理系统中,就只需要姓名、年龄、性别、身体证号等信息,而人有哪些爱好之类的属性就不需要被关注。
简而言之,抽象数据类型就是在一个场景中,把现实世界中的某个事物的某些属性映射到计算机世界中,并用相应的更基本的数据类型来表示,以此来更方便地对信息数据进行处理。
线性表的基本特征
线性表是一种由相同类型的数据元素组成的具有顺序的有限序列,每个线性表实例都有自己的类型,可能储存和自己类型相同或者继承于自己类型的数据元素才能添加到线性表中。线性表可以很方便地管理数据元素集合,可以添加、插入到特定位置,可以替换删除,当然还有最基本的取元素。线性表可以像数组那样按下标位置取元素。在真实的开发环境中,线性表应该是除了数组之外使用最多的一种集合类型,数组也许都不能称之为集合类型,因为数组是编程语言提供的一种数据类型,在C/C++中就是一个指针。线性表比数组的优势很明显,数组是固定长度的,而线性表可以动态扩容,当然在正常情况下线性表也会更耗内存,因为线性表要申请额外的内存来保证申请内存的次数尽量少,采用这样用空间来换时间的策略。但由于线性表可以扩容,在修改操作较频繁的系统中,使用线性表会比使用数组高效得多,因为如果采用链表实现的线性表,会比数组少元素移动和数组copy的操作。
根据线性表的基本特性,可以发现线性表有两种实现方法,分别是顺序储存结构和链式储存结构。
其中,顺序储存结构是充分利用了计算机硬件储存设备的顺序读写的特性。不管是硬盘还是内存条,计算机在读写数据的时候都是维持了一个读写指针,很多时候这个读写指针是物理存在的。如果采用顺序储存结构,那么在读写的时候就可以让读写指针按顺序地进行扫描了,不用来回地移动指针来寻找特定的位置,这样大大减少了物理寻址时间。
而链式储存结构,就是把一个数据元素结点存放在任意的位置(当然,不可能是绝对的任意位置,操作系统会做相应的优化的限制),在数据集合中逻辑相邻的元素可以在储存中相隔很远,只是保存了相邻元素的地址。如果想到访问相邻元素的话,就根据这个地址再去查找就好了。这样的好处是在集合修改时,由于元素的物理位置是分散的,修改了集合的逻辑结构并不会破坏其物理结构,而如果使用顺序储存结构时,逻辑上的顺序结构被破坏就表示物理顺序结构也被破坏了,就要做相应的补救措施。比如在线性表的插入操作时,使用顺序储存结构,向表中间插入一个元素,就需要把插入位置之后所有元素都向后移动一位,让待插入元素放在移动后空出来的这个位置。而如果使用链式储存结构的话,就像一条铁链要在中间加一节一样,只需要在中间把两节断开,然后让待加入的前端连上断开的前面的那一节的后端,让待加入那节的后端连上断开的后面那一节的前端。这样其实只操作了原数据的两个元素,操作过程少了,所用的时间也相应的就少了,效率就相应高了。而如果要顺序遍历的话,链式储存结构就要不断地根据当前元素中存储的下一元素的位置去找下一个元素,在物理存储中就需要读写指针不断地回来移动查找,因为就算两个元素逻辑上相邻,它们物理上也不一定相邻,但是采用顺序储存结构的话,指针就只需要从一端顺序移向另一端了。
从顺序储存结构和链式储存结构的特点可以看出,没有绝对好的存储结构的选择,只能在特定的场景中选择更适合场景的储存方式。比如在查找操作频繁的情况下,就采用顺序储存结构,如果是在修改操作频繁的情况下,就采用链式存储结构。
为了方便选择和操作,可以使用C++面向对象的特性,定义线性表的基本操作,而把具体的储存结构延迟到子类中去实现。这样,我们在使用时就面向的是线性表,而不是具体的哪种存储结构,不需要关心底层是如何实现的,这样会大大提高工程式开发的效率。还可以使用C++支持泛型的特性来把线性表设计成模板类,做成真正的抽象数据类型,这样可以让元素不同类型时可以使用同一套线性表实现,这样在工程开发时可以减少开发代码量,而且更方便管理与维护。以上两点在计算机软件设计中表现为针对抽象而不是针对细节编程以及抽象具有更低的耦合性的两个原则。
下面是用C++实现的基于顺序储存结构和链式储存结构的线性表实现
#pragma once
#ifndef LIST_H
#define LIST_H
#include <iostream>
using namespace std;
namespace dataStructure
{
template<typename T>
class List
{
public:
List() {};
virtual ~List() {};
virtual int Length()const = 0;
virtual bool Empty()const = 0;
virtual void Clear() = 0;
virtual void Traverse(void(*visit)(T &t))const = 0;
virtual void GetElem(const int position, T &t) = 0;
virtual bool SetElem(const int position, const T &t) = 0;
virtual bool Delete(const int position, T &t) = 0;
virtual bool Insert(const int position, const T &t) = 0;
virtual void Add(const T &t) = 0;
};
}
#endif
#pragma once
#pragma once
#ifndef ARRAYLIST_H
#define ARRAYLIST_H
#include "list.h"
namespace dataStructure
{
template<typename T>
class ArrayList :public List<T>
{
public:
ArrayList();
~ArrayList();
int Length()const;
bool Empty()const;
void Clear();
void Traverse(void(*visit)(T &t))const;
void GetElem(const int position, T &t);
bool SetElem(const int position, const T &t);
bool Delete(const int position, T &t);
bool Insert(const int position, const T &t);
void Add(const T &t);
private:
const int DEFAULT_SIZE = 1000;//初始大小
const int INCRE_SIZE = 100;//扩容大小
int maxSize = DEFAULT_SIZE;//初始最大容量
T* data;
int size;
};
template<class T>
ArrayList<T>::ArrayList()
{
data = new T[DEFAULT_SIZE];
}
template<class T>
ArrayList<T>::~ArrayList()
{
delete data;
}
template<class T>
int ArrayList<T>::Length()const
{
return size;
}
template<class T>
bool ArrayList<T>::Empty()const
{
return size == 0;
}
template<class T>
void ArrayList<T>::Clear()
{
size = 0;//只需要将数量清空即可,因为删除或查询时要先验证数量
}
template<class T>
void ArrayList<T>::Traverse(void(*visit)(T &t))const
{
for (int i = 0;i < size;i++)
{
visit(data[i]);
}
}
template<class T>
void ArrayList<T>::GetElem(const int position, T &t)
{
if (position >= 0 || position < size)
{
t = data[position];
}
}
template<class T>
bool ArrayList<T>::SetElem(const int position, const T &t)
{
if (position < 0 || position >= size)
{
return false;
}
data[position] = t;
return true;
}
template<class T>
bool ArrayList<T>::Delete(const int position, T &t)
{
if (position <0 || position >= size)
{
return false;
}
for (int i = position;i < size - 1;i++) {
data[i] = data[i + 1];
}
size--;
return true;
}
template<class T>
bool ArrayList<T>::Insert(const int position, const T &t)
{
if (position < 0 || position > size) //插入的位置非法
{
return false;
}
if (size >= maxSize)//扩容
{
maxSize += INCRE_SIZE;
T *temp = new T[maxSize];
memcpy(temp, data, sizeof(data));
delete data;//释放原来的数组内存
data = temp;
}
if (position == size)//插入到末尾
{
data[position] = t;
}
else//插入在中间或开头
{
for (int i = position;i < size;i++)
{
data[i + 1] = data[i];
}
data[position] = t;
}
size++;
return true;
}
template<typename T>
void ArrayList<T>::Add(const T &t)
{
if (size >= maxSize)//扩容
{
maxSize += INCRE_SIZE;
T *temp = new T[maxSize];
memcpy(temp, data, sizeof(data));
delete data;//释放原来的数组内存
data = temp;
}
data[size++] = t;
}
}
#endif
#pragma once
#pragma once
#ifndef LINKLIST_H
#define LINKLIST_H
#include "list.h"
namespace dataStructure
{
template<typename T>
struct Node
{
T data;
Node<T> *next = NULL;
Node<T> *pre = NULL;
};
template<typename T>
class LinkList :public List<T>
{
public:
LinkList();
~LinkList();
int Length()const;
bool Empty()const;
void Clear();
void Traverse(void(*visit)(T &t))const;
void GetElem(const int position, T &t);
bool SetElem(const int position, const T &t);
bool Delete(const int position, T &t);
bool Insert(const int position, const T &t);
void Add(const T &t);
private:
Node<T> *header;//头节点不储存数据
int size;
};
template<class T>
LinkList<T>::LinkList()
{
header = new Node<T>;
header->pre = NULL;
header->next = NULL;
}
template<class T>
LinkList<T>::~LinkList()
{
Node<T>* node = header;
Node<T>* next = node->next;
while (node)
{
delete node;
node = next;
next = node->next;
}
}
template<class T>
int LinkList<T>::Length()const
{
return size;
}
template<class T>
bool LinkList<T>::Empty()const
{
return size == 0;
}
template<class T>
void LinkList<T>::Clear()
{
size = 0;//只需要将数量清空即可,因为删除或查询时要先验证数量
}
template<class T>
void LinkList<T>::Traverse(void(*visit)(T &t))const
{
Node<T> *node = header->next;
int index = 0;
while (node && index++ < size)
{
visit(node->data);
node = node->next;
}
}
template<class T>
void LinkList<T>::GetElem(const int position, T &t)
{
if (position >= 0 && position < size)
{
Node<T> *node = header->next;
int index = 0;
while (node && (index++) != position)
{
node = node->next;
}
if (node)
{
t = node->data;
}
}
}
template<class T>
bool LinkList<T>::SetElem(const int position, const T &t)
{
if (position < 0 || position >= size)
{
return false;
}
Node<T> *node = header->next;
int index = 0;
while(node && (index++) != position)
{
node = node->next;
}
if(node)
{
node->data = t;
}
else {
return false;
}
return true;
}
template<class T>
bool LinkList<T>::Delete(const int position, T &t)
{
if (position <0 || position >= size)
{
return false;
}
Node<T> *node = header->next;
int index = 0;
while(node && (index++) != position)
{
node = node->next;
}
if(node)
{
if(node->pre)
{
node->pre->next = node->next;//将当前节点的前一个节点的后驱指针指向后一个节点
}
if(node->next)
{
node->next->pre = node->pre;//将当前节点的后一个节点的前驱指针指向前一个节点
}
delete node;
size--;
}else
{
return false;
}
return true;
}
template<class T>
bool LinkList<T>::Insert(const int position, const T &t)
{
if (position < 0 || position > size) //插入的位置非法
{
return false;
}
Node<T> *newNode = new Node<T>;
newNode->data = t;
//找到要插入位置的前一个节点
Node<T> *node = header;
if (size != 0 && position > 0)//列表不为空并且不是插在首个位置上
{
int index = 0;
node = header->next;
while (node && (index++) != position - 1)
{
node = node->next;
}
}
newNode->pre = node;
newNode->next = node->next;
node->next = newNode;
size++;
return true;
}
template<typename T>
void LinkList<T>::Add(const T &t)
{
//查找最后一个节点
Node<T> *node = header;
while(node->next)
{
node = node->next;
}
Node<T> *newNode = new Node<T>;
newNode->data = t;
newNode->pre = node;
node->next = newNode;
size++;
}
}
#endif
#include "stdafx.h"
#include "arraylist.h"
#include "linklist.h"
using namespace dataStructure;
void visit(int &t)
{
cout << t << ",";
}
int main()
{
List<int> *list = new ArrayList<int>;
list->Add(30);
cout << list->Empty() << endl;
list->Add(10);
list->Insert(1, 20);
list->Traverse(visit);
cout << endl;
list->Clear();
cout << list->Empty() << endl;
list->Traverse(visit);
system("pause");
return 0;
}
在main函数中,可以直接把ArrayList替换成LinkList,程序可以执行相同的逻辑,而底层使用的数据结构是不同的。