线性表__数据结构笔记

线性表是最基本、最简单、也是最常用的一种数据结构。线性表(linear list)是数据结构的一种,线性表是n个数据元素构成的有限序列,表中元素的数量为表的长度。长度为零的表为空表。

特点:

  • 集合中必存在唯一的一个“第一元素”。

  • 集合中必存在唯一的一个 “最后元素” 。

  • 除最后一个元素之外,均有唯一的后继(后件)。

  • 除第一个元素之外,均有唯一的前驱(前件)。

逻辑结构图:

a1————a2————a3————a4--------------an

存储结构:
在这里插入图片描述

一、顺序存储

1.顺序表

使用数组实现,一组地址连续的存储单元,数组大小有两种方式指定,一是静态分配,二是动态扩展。

注:线性表从1开始,而数组从0开始。

优点:

  • 有随机访问特性,查找O(1)时间
  • 无需额外空间,存储密度高

缺点:

  • 插入和删除操作需移动大量元素
  • 表的容量难以确定

顺序表的实现

用C++的类来实现顺序表,由于数据类型不确定,所以采用模板机制。

const int MAXSIZE = 1e5;
template<typename T>
class SeqList {
	T data[MAXSIZE];
	int length;
public:
	SeqList();
	SeqList(T a[], int n);
	~SeqList();
	int GetLength();//求线性表的长度
	T Get(int index);//按索引查找
	int Locate(T x);//按值查找
	void Insert(T x, int index);//插入
	T Delete(int index);//按位置删除
	bool Empty();//判断是否为空
	void printSetList();//遍历输出
};

无参构造函数—初始化顺序表

建立一个空的顺序表,将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;
}

析构函数—销毁顺序表

顺序表是静态储存分配,自动释放内存。所以析构函数为空。

template<typename T>
SeqList<T>::~SeqList()
{
	//空
}

求线性表的长度

返回length的值即可

template<typename T>
int SeqList<T>::GetLength()
{
	return length;
}

按位查找

顺序表下表为i的元素储存在数组中下标为i-1的位置。时间复杂度为O(1)。

template<typename T>
T SeqList<T>::Get(int index)
{
	if (index < 0 || index >= length)
		throw"非法输入!";
	return data[index - 1];
}

按值查找

需要对顺序表中的元素进行比较,查找成功返回序号(下标+1),否则返回0。时间复杂的为O(n)。

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,待插入的元素值x
输出:如果插人成功,返回新的顺序表,否则返回插入失败信息
1.如果表满了,则输出上溢错误信息,插入失败;
2.如果元素的插人位置不合理,则输出位置错误信息,插人失败;
3.将最后一个元素直至第1个元素分别向后移动一个位置;
4.将元素x填人位置i处;
5.表长加1;
时间复杂度为O(n)

template<typename T>
void SeqList<T>::Insert(T x, int index)
{
	if (length = MAXSIZE)throw("上溢!");
	if (index < 0 || index >= length)throw("非法输入!");
	for (int i = length; i >= index; i--)
		data[i] = data[i - 1];
	data[index - 1] = x;
	length++;
}

删除操作

从删除位置开始,将数据元素前移一位即可,注意下标的表示。时间复杂度为O(n)

template<typename T>
T SeqList<T>::Delete(int index)`在这里插入代码片`
{
	if (length = 0)throw("下溢!");
	if (index < 0 || index >= length)throw("删除位置错误!");
	T x = data[index - 1];
	for (int i = index; i < length; i++)
		data[i - 1] = data[i];
	length--;
	return x;
}

判断顺序表是否为空

判断length的值即可

template<typename T>
bool SeqList<T>::Empty()
{
	if (length > 0)
		return true;
	return false;
}

遍历输出

template<typename T>
void SeqList<T>::printSetList()
{
	for (int i = 0; i < length; i++)
	{
		cout << data[i] << " ";
		if (i % 5 == 0)
			cout << endl;
	}
}

二、链式存储

1.单链表

单链表是用一组任意的存储单元存放线性表的元素,这组存储单元可以连续也可以不连续,甚至可以零散分布在内存中的任意位置。

每个存储单元在存储数据元素的同时,还必须存储其后继元素所在的地址信息,这个地址信息称为指针。这两部分组成了数据元素的存储映象,称为结点

每个结点的存储地址存放在其前驱结点的next域中,而第一个元素无前驱,所以设头指针指向第一个元素所在结点(称为开始结点),整个单链表的存取必须从头指针开始进行,因而头指针具有标识一个单链表的作用。最后一个元素无后继,故最后一个元素所在结点(称为终端结点)的指针域为空,这个空指针称为尾标志。

通常在单链表的开始结点之前附设一个类型相同的结点,称为头结点。也有不带头结点的单链表,不过其操作更为麻烦,所以不经常使用,我们下面会简单介绍。如不特殊说明,使用的链表都是默认带头结点的。

单链表的实现

结点的定义

data为数据区,存放数据
next为指针域,存放下一个结点的地址

template<typename T>
struct Node 
{
	T data;
	Node<T>* next; 
};

声明:

template<typename T>
class LinkList {//有首结点
	Node<T>* first;//头指针
public:
	LinkList();
	LinkList(T a[], int n);//头插法
	LinkList(int n,T a[])//尾插法
	T Get(int index);//按位查找
	int Locate(T x);//按值查找
	void Insert(int index, T x);//插入
	T Delete(int index);//删除
	void Delete_Data(T x);//按值删除
	void PrintList();//遍历输出
	int Length();//链表长度
	int Empty();//判断链表是否为空
	~LinkList();//析构函数
};

无参构造

带头结点

创建一个带首节点的空链表
给first指针申请一块内存,将其指针域赋空值(代表无后继结点),即空链表

template<typename T>
LinkList<T>::LinkList()
{
	first = new Node<T>;
	first->next = NULL;
}

不带头结点

直接把头指针赋空值

template<typename T>
LinkList<T>::LinkList()
{
	first = NULL;
}

有参构造(头插法)

带头结点

头插法就是把结点插入到首节点之后
先建立结点s储存数据
将s的指针域指向原来的第一个结点,即first->next
现在s成为了第一个节点
将first->next指向s

template<typename T>
LinkList<T>::LinkList(T a[], int n)
{
	first = new Node<T>;
	first->next = NULL;
	Node<T>* s = NULL;
	for (int i=0; i < n; i++)
	{
		s = new Node<T>;
		s->data = a[i];
		s->next = first->next;
		first->next = s;
	}
}

不带头节点

把插入的节点作为首结点

template<typename T>
LinkList<T>::LinkList(T a[], int n)
{
	first = NULL;
	Node<T>* s = new Node<T>;

	for (int i = 0; i < n; i++)
	{
		s = new Node<T>;
		s->data = a[i];
		s->next = first;
		first = s;
	}
}

有参构造(尾插法)

带头结点

尾插法就是在链表的尾部插入节点
为了操作方便,这里需要增加一个尾指针last
定义一个结点s储存信息
将尾指针的指针域指向新插入的s
现在结点s才是真正的队尾
然后再将尾指针指向s
所有元素都插入后,将尾指针的next赋空值

template<typename T>
LinkList<T>::LinkList(int n, T a[])
{
	first = new Node<T>;
	first->next = NULL;
	Node<T>* s = NULL;
	Node<T>* last = first;
	for (int i = 0; i < n; i++)
	{
		s = new Node<T>;
		s->data = a[i];
		last->next = s;
		last = s;
	}
	last->next = NULL;
}

不带头节点

操作相对来说较复杂一些,第一个节点和后续结点的操作不同
需要先插入第一个结点,再循环插入后续结点

template<typename T>
LinkList<T>::LinkList(T a[],int n)
{
	Node<T>* s = new Node<T>;
	s->data = a[0];
	first = s;
	Node<T>* last = first;
	for (int i = 1; i < n; i++)
	{
		s = new Node<T>;
		s->data = a[i];
		last->next=s;
		last = s;
	}
	last->next = NULL;
}

按位置查找值

这里需要一个工作指针p和一个计数器count来计数
当count==index-1时,此时的p就是要查找的结点
输出p->data

template<typename T>
T LinkList<T>::Get(int index)
{
	int count = 0;//计数器
	Node<T>* p = first->next;//工作指针

	while (p && count < index-1)
	{
		count++;
		p = p->next;//工作指针后移
	}
	if (!p)throw"查找失败";
	return p->data;
}

按值查找位置

还是需要计数器count来计数,当p->next==x时,输出此时的count

template<typename T>
int LinkList<T>::Locate(T x)
{
	Node<T>* p = first->next;
	int count = 1;
	while (p)
	{
		if (p->data == x)return count;//查找成功
		count++;
		p = p->next;
	}
	if (!p)throw"查找失败";
}

插入操作

有首节点

使用结点s来记录数据
插入需要查找到第i-1个结点,然后再修改指针变量的值
将s->next指向下一个结点,即第i-1个结点储存的地址(next)
然后将第i-1个结点的next指向s

template<typename T>
void LinkList<T>::Insert(int index, T x)
{
	Node<T>* p = first;
	int count = 0;

	while (p && count < index - 1)
	{
		p = p->next;
		count++;
	}
	if (p)
	{
		Node<T>* s = new Node<T>;
		s->data = x;
		s->next = p->next;
		p->next = s;
	}
	else
		throw"插入失败";
}

无首结点

需要判断插入的位置,插入在第一个元素的操作不同

template<typename T>
void LinkList<T>::Insert(int index, T x)
{
	Node* p=NULL;
	Node* s = NULL;
	int count;
	if (index <= 0) throw"位置非法";
	if (index == 1)
	{
		s = new Node<T>;
		s->data = x;
		s->next = first;
		first = s;
		return;
	}

	p = first;
	count = 1;
	while (p && count < i - 1)
	{
		p = p->next;
		count++;
	}
	if (!p)throw"位置非法";
	else
	{
		s = new Node<T>;
		s->data = x;
		s->next = p->next;
		p->next = s;
	}
}

按位置删除

删除操作也是需要找到第i-1个结点
然后将第i-1个结点的next指向第i+1个结点,即第i个结点的next
然后释放第i个结点的内存

template<typename T>
T LinkList<T>::Delete(int index)
{
	T x;
	Node<T>* p = first;
	Node<T>* q = NULL;
	int count = 0;
	while (p && count < index - 1)
	{
		p = p->next;
		count++;
	}
	if (!p || !p->next)"删除失败";
	else
	{
		q = p->next;
		x = q->data;
		p->next = q->next;
		delete q;
		return x;
	}
}

按值删除

先查找到和数据相等的值所在的结点p
再修改其前驱结点p的的指针域

void LinkList::Delete_Data(int x)//按值删除
{
    Node* p = first;//前驱
    Node* q = NULL;//后继
    Node* temp = NULL;
    bool flag = false;

    while (p->next)
    {
        if (p->next->data == x)
        {
            q = p->next;
            p->next = q->next;
            delete q;
            flag = true;
        }
       p = p->next;
    }
    if (!flag)throw"删除失败";
}

求链表的长度

遍历计数即可

template<typename T>
int LinkList<T>::Length()
{
	Node<T>* p = first->next;
	int count = 1;
	while (p)
	{
		count++;
		p = p->next;
	}
	return count;
}

判断链表是否为空

判断first->next是否为空

template<typename T>
int LinkList<T>::Empty()
{
	if (first->next == NULL)
		return 1;
	return 0;
}

输出链表

遍历输出即可

template<typename T>
void LinkList<T>::PrintList()
{
	Node<T>* p = first->next;
	while (p)
	{
		cout << p->data <<" ";
		p = p -> next;
	}
	cout << endl;
}

析构函数

释放内存

template<typename T>
LinkList<T>::~LinkList()
{
	Node<T>* p = NULL;
	while (first)
	{
		p = first->next;
		delete first;
		first = p;
	}
}

2.循环链表

将单链表的头尾结点连接起来,就是一个循环链表。没有增加额外的开销,为链表的操作增添了不少便利。
特点:从链表的任意一点出发,都能访问到表中其他节点。

整体的方法和单链表的差异不大,只要稍作修改即可。

构造空表

首尾相连

template<typename T>
CycleLinkList<T>::CycleLinkList()
{
	first = new Node<T>;
	first->next = first;
}

有参构造(头插法)

带头结点

和单链表构造的唯一差距就是将其中一条语句修改
first->next=first

template<typename T>
LinkList<T>::LinkList(T a[], int n)
{
	first = new Node<T>;
	first->next = first;//修改
	Node<T>* s = NULL;
	for (int i=0; i < n; i++)
	{
		s = new Node<T>;
		s->data = a[i];
		s->next = first->next;
		first->next = s;
	}
}

有参构造(尾插法)

带头结点

last->next=first

template<typename T>
LinkList<T>::LinkList(int n, T a[])
{
	first = new Node<T>;
	first->next = NULL;
	Node<T>* s = NULL;
	Node<T>* last = first;
	for (int i = 0; i < n; i++)
	{
		s = new Node<T>;
		s->data = a[i];
		last->next = s;
		last = s;
	}
	last->next = first;
}

将单链表变为循环链表

Node*p=first;
while(p->next)
{
	p=p->next;
}
p->next=first;

其他的操作与单链表的操作基本相同们这里不再赘述。

3.双链表

在双链表中,每个结点不仅记录了数据元素,还储存了前驱结点和后继结点的地址信息。能方便的找到前驱节点。

结点构成

template<typename T>
struct Node
{
	T data;
	Node<T>* pre;
	Node<T>* next;
};

声明

template<typename T>
class DoubleLink {

	Node<T>* first;
public:
	DoubleLink();
	DoubleLink(T a[], int n);
	T Get(int index);
	int Get_data(T x);
	T Delete(int index);
	T Delete_data(T x);
	void Insert(int index, T x);
	int Length();
	void Print();
	~DoubleLink();
};

无参构造

template<typename T>
DoubleLink<T>::DoubleLink()
{
	first = new Node<T>;
	first->pre = NULL;
	first->next = NULL;
}

头插法构造

先正向修改指针,再反向修改指针,同时注意判断p->next是否存在

template<typename T>
DoubleLink<T>::DoubleLink(T a[],int n)
{
	first = new Node<T>;
	first->pre = NULL;
	first->next = NULL;
	Node<T>* s = NULL;
	for (int i = 0; i < n; i++)
	{
		s = new Node<T>;
		s->data = a[i];
		s->next = first->next;
		first->next = s;
		s->pre = first;
		if(s->next)
			s->next->pre = s;
	}
}

尾插法构造

利用尾指针进行操作

template<typename T>
DoubleLink<T>::DoubleLink(T a[], int n)
{
	first = new Node<T>;
	first->pre = NULL;
	first->next = NULL;
	Node<T>* s = NULL;
	Node<T>* last = first;

	for (int i = 0; i < n; i++)
	{
		s = new Node<T>;
		s->data = a[i];
		last->next = s;
		s->pre = last;
		last = s;
	}
	last->next = NULL;
}

按位置查找值

和单链表操作相同

template<typename T>
T LinkList<T>::Get(int index)
{
	int count = 0;//计数器
	Node<T>* p = first->next;//工作指针

	while (p && count < index-1)
	{
		count++;
		p = p->next;//工作指针后移
	}
	if (!p)throw"查找失败";
	return p->data;
}

按值查找位置
和单链表操作相同

template<typename T>
int LinkList<T>::Locate(T x)
{
	Node<T>* p = first->next;
	int count = 1;
	while (p)
	{
		if (p->data == x)return count;//查找成功
		count++;
		p = p->next;
	}
	if (!p)throw"查找失败";
}

插入操作

和单链表基本相同,就只有修改指针操作不同

template<typename T>
void DoubleLink<T>::Insert(int index, T x)
{
	if (index <= 0)throw"位置非法";
	Node<T>* p = first;
	int count = 0;
	while (p&&count<index-1)
	{
		p = p->next;
		count++;
	}
	if (p)
	{
		Node<T>* s = new Node<T>;
		s->data = x;
		s->next = p->next;
		p->next = s;
		s->pre = p;
		if (s->next)
			s->next->pre = s;
	}
	else
		throw"位置非法"
	
}

按位置删除
和单链表稍微有些不同
不需要找前驱节点,直接找到第index个结点就可以

template<typename T>
T DoubleLink<T>::Delete(int index)
{
	T x;
	Node<T>* p = first->next;
	int count = 0;
	while (p && count < index - 1)
	{
		p = p->next;
		count++;
	}
	if (!p || !p->next)"删除失败";
	else
	{
		p->pre->next = p->next;
		if(p->next)
			p->next->pre = p->pre;
		delete p;
		return x;
	}
}

按值删除

思路和单链表基本上相同,知识细微部分不同
需要注意判断p->next是否为空

void List::Delete(int x)
{
    Node* p=NULL, * q=NULL;
    p = first->next;
    while (p)
    {
        if (p->data == x)
        {
            q = p;
            p->pre->next = p->next;
            if (p->next != NULL)
                p->next->pre = p->pre;
            delete q;
        }
        p = p->next;
    }
}

遍历输出

和单链表操作相同

template<typename T>
void DoubleLink<T>::Print()
{
	Node<T>* p = first->next;
	while(p)
	{
		cout << p->data << " ";
		p = p->next;
	}
	cout << endl;
	return;
}

求链表的长度

遍历计数,和单链表相同

template<typename T>
int LinkList<T>::Length()
{
	Node<T>* p = first->next;
	int count = 1;
	while (p)
	{
		count++;
		p = p->next;
	}
	return count;
}

析构函数

和单链表相同

template<typename T>
DoubleLink<T>::~DoubleLink()
{
	Node<T>* p = NULL;
	while (first)
	{
		p = first->next;
		delete first;
		first = p;
	}
}

总的来说,双链表的操作的实现思路和单链表相同,操作上大同小异。

三、顺序表和链表的比较

时间性能:

若线性表的操作主要是查找,很少进行删除、插入时,可以选择顺序表来储存。
若进行频繁的插入和删除操作,适合选择链表来储存。

空间性能:

当线性表的长度变化不大,且事先容易确定其大小时,适合采用顺序表来存储,否则使用链表。

如果你觉得看完这篇文章有收获,就动动小手点个赞吧!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值