目录
线性表的逻辑结构
线性表(表),n(>=0)个数据元素的有限序列,元素个数=线性表的长度,长度为0称空表。
第一个元素无前驱,最后一个元素无后继,其他元素有且仅有一个前驱和一个后继。
ADT List:初始化、建立线性表、销毁、遍历、求长度、按值查找、按位查找、插入、删除。
顺序表
线性表的顺序存储结构。
上图为顺序表中元素ai的存储地址
计算任意一个元素的存储地址的时间是相等的存储结构为随机存取结构。(存储地址是其序号的线性函数)
通常用一维数组实现!但是c++的数组下标从0,开始,所以第i个元素存在下标为i-1的位置。数组需要分配固定长度的数组空间,所以要确定存放线性表的数组空间的长度。(Length<MaxSize)
const int MaxSize = 100;//顺序表最大长度,根据实际问题定义。
template<typename T>
class SeqList
{
T data[MaxSize];
int length;
public:
SeqList();//无参构造函数初始化
SeqList(T a[], int n);//有参构造函数初始化
~SeqList();
int Length();//获取列表长度
int Empty();//判空
T Get(int i);//按位查找
int Locate(T x);//按值查找
void Insert(T x, int i);//插入
T Delete(int i);//删除
void printList();//遍历
};
无参构造函数
length初始化为0。
template <typename T>
SeqList<T>::SeqList()
{
length = 0;
}
有参构造函数
创建顺序表,但是顺序表的存储空间不能小于给定的元素个数,不然建立不了顺序表。
顺序表表长就等于传入数组的长度。
template <typename T>
SeqList<T>::SeqList(T a[],int n)
{
if (n > MaxSize)throw"参数非法";
for (int i = 0; i < n; i++)
{
data[i] = a[i];
}
length = n;
}
析构函数
顺序表退出作用域时自动释放该变量所占的内存单元,所以顺序表不用销毁~
获取列表长度:
直接return length就好~
判空:length = 0 为空
查找
按位查找:要注意参数i是否合法。
template<typename T>
T SeqList<T>::Get(int i)
{
if (i<1 || i>length)throw"参数非法";
else return data[i-1];
}
按值查找:
template<typename T>
int SeqList<T>::Locate(T x)
{
for (int i = 0; i < length; i++)
{
if (data[i] == x)return i + 1;
}
return 0;//表示没有找到
}
插入
找到插入的位置i(a3处),后面元素都向后移一位,再将元素填入i处。
template<typename T>
void SeqList<T>::Insert(T x, int i)
{
if (length == MaxSize)throw"上溢";
if (i<1 || i>length)throw"插入位置错误";
for(int j = length; j>=i;j--)
data[j] = data[j - 1];
data[i - 1] = x;
length++;
}
时间复杂度:O(n)
每个位置插入的可能性都一样,那么概率为1/n+1,在这n+1的位置上平均向后移的次数为n*(n+1)/2,所以为时间复杂度为O(n/2)即O(n)。
删除
向前移,注意下溢!
template<typename T>
T SeqList<T>::Delete(int i)
{
if (length == 0)throw"下溢";
if (i > length || i < 1)throw"删除位置错误";
x = data[i - 1];
for (int j = i; j < length; j++)
{
data[j - 1] = data[j];
}
length--;
return x;
}
遍历
把数组都遍历一遍噜~
链表
单链表
用一组任意的存储单元存放线性表内的元素,存储单元可连续可不连续,甚至可以零散分布。
头指针:第一个元素无前驱,所以设置头指针指向第一个元素所在的结点。
整个单链表的存取必须从头指针开始,头指针具有标识一个单链表的作用。
尾标志:最后一个元素无后继,所以最后一个元素所在结点的指针域为空。这个空指针就叫尾标志
头结点:通常单链表的开始节点之前设一个类型相同的结点。无论单链表是否为空,头指针永远指向头结点。
template<typename T>
struct Node
{
T data;
Node<T>* next;
};
template<typename T>
class LinkList
{
Node* head;
public:
LinkList();//无参构造函数
LinkList(T a[], int n);//有参构造函数
~LinkList();
int Length();//获取链表长度
int Empty();//判空
T Get(int i);//按位查找
int Locate(T x);//按值查找
void Insert(T x, int i);//插入
T Delete(int i);//删除
void printList();//遍历
};
初始化
无参构造函数
template<typename T>
LinkList<T>::LinkList()
{
head = new Node<T>; //生成头结点
head->next = nullptr;//头结点的指针域置空
}
有参构造函数
1.头插法建立单链表
template<typename T>
LinkList<T>::LinkList(T a[], int n)
{
head = new Node<T>;
head->next = nullptr;//初始化空链表
for (int i = 0; i < n; i++)//头插法
{
Node* s = new Node<T>;
a[i] = s->data;
s->next = head->next;
head->next = s;
}
}
2.尾插法建立单链表
template<typename T>
LinkList<T>::LinkList(T a[], int n)
{
head = new Node<T>;
Node<T>* r = head, * s = nullptr;
for (int i = 0; i < n; i++)
{
s = new Node<T>;
s->data = a[i];
r->next = s;
r = s;
}
r->next = nullptr;
}
求长度
template<typename T>
int LinkList<T>::Length()
{
Node<T>* p = head->next;
int count = 0;
while (p != nullptr)
{
p = p->next;
count++;
}
return count;
}
判空:直接看head->next是不是为空。
按位查找
template<typename T>
T LinkList<T>::Get(int i)
{
NNode<T>* p = head->next;
int count = 1;
while (p != nullptr && count < i)
{
p = p->next;
count++;
}
if (p == nullptr)throw"查找位置错误";
return p->data;
}
按值查找
template<typename T>
int LinkList<T>::Locate(T x)
{
Node<T>* p = head->next;
int count = 1;
while (p != nullptr)
{
if (p->data == x)return count;//找到啦!
p = p->next;
count++;
}
return 0;//说明查找元素不存在(查找失败)
}
插入
中间插入
template<typename T>
void LinkList<T>::Insert(T x, int i)
{
Node<T>* p = head->next, * s = nullptr;
int count = 1;
while (p != nullptr && count < i)
{
p = p->next;
count++;
}
if (p == nullptr)throw"插入位置错误";
else
{
s = new Node<T>;
s->data = x;
s->next = p->next;
p->next = s;
}
}
插入操作的时间复杂度:O(n)
删除
中间删除
template<typename T>
T LinkList<T>::Delete(int i)
{
Node<T>*p = head->next,* q = nullptr;
T x;
int count = 1;
while (p != nullptr&&count<i-1)//找到第i-1个结点
{
p = p->next;
count++;
}
if (p == nullptr||p->next==nullptr)throw"删除位置错误";//考虑后继不存在
else
{
q = p->next;
x = q->data;
p->next = q->next;
delete q;
return x;
}
}
删除操作的时间复杂度:O(n)
遍历不多说了吧。应该都知道啦~
析构函数
单链表为动态存储分配,需要释放空间。
template<typename T>
LinkList<T>::~LinkList()
{
Node<T>* p = head;
while (head != nullptr)
{
head = head->next;
delete p;
p = head;
}
}
双向链表
可以快速找到单链表中任一结点的前驱结点,可以在单链表的每个结点中再设置一个指向其前驱节点的指针域。
template<typename T>
struct DulNode
{
T data;
DulNode<T>* prior, * next;//多了prior指针
};
插入:
1:s->next = p->next;
2:s->prior = p;
3:p->next->prior =s;
4:p->next = s;
删除:
1:p->prior->next = p->next;
2:p->next->prior= p->prior;
3:delete p;
循环链表
循环单链表:
在第一个结点处插入的时间复杂度:O(1)
在最后一个结点处插入的时间复杂度:O(n)
循环单链表设尾指针:设置一个指向最后一个元素的指针,那么查找开始结点和终端结点都很方便。(实际上用的多的也是有尾指针的循环单链表)
在第一个结点处插入的时间复杂度:O(1)
在最后一个结点处插入的时间复杂度:O(1)
循环双链表:
在第一个结点处插入的时间复杂度:O(1)
在最后一个结点处插入的时间复杂度:O(1)
循环双链表比带尾指针的循环单链表多了一些指针,这些指针也需要分配储存空间。虽然循环链表提高了链表操作的灵活性,但是这种方法危险在于没有明显的尾端,易陷入死循环。
顺序表和链表的比较
顺序表和链表没有好坏之分,要根据实际问题去使适合的存储结构。
顺序表的优点
- 空间利用率高。
- 随机存取,查找直接根据索引查找,十分方便。(O(1))
顺序表的缺点
- 插入删除比较慢,每次插入都要遍历。
- 插入时需要判断是否需要扩容,扩容会消耗系统性能。
- 存储元素过少浪费空间,存储元素过多需要扩容。
链表的优点
- 插入删除比较快,改变next指针的指向好。
- 没有空间限制,存放元素比较自由,只要存储器有空间就不会出现溢出和考虑扩容的问题。
链表的缺点
- 不能随机存取,访问元素需要遍历。
- 需要占用额外空间去存储next指针域。
使用的书籍:《数据结构——从概念操C++实现(第三版)》王红梅 王慧 王新颖 编著
(最后顺序表和链表的比较是从复习题解析中copy出来的,感谢老师的总结~)