1、前言
线性表中的顺序结构中的它表现出来的问题已经很明确了,就是我们在插入和删除时候,需要移动大量的元素。我们对一个线性表的操作中,最常使用操作无非就是插入、删除、查找这三种,因为顺序表示的时间复杂度无法满足我们追求的快速的要求。为了写出更加高效的代码,于是前辈们就想到了使用链表结构来表示一个线性表
2、链表结构
(1)链表结构包括两个域:指针域和数据域
(2)我们根据指针域的多少,把链表分为单链表和双链表。根据链表的首尾是否相连,把链表分为循环和非循环链表。
3、链表和顺序结构(数组)优缺点
数组的优点:
(1):能够实现元素的随机访问,效率高,时间复杂度为:O(1)
(2):在查找元素时,如果数组有序的话,可以采取二分查找,时间复杂度为:O(logn),
如果无序的话时间复杂为:O(n)
数组的缺点:
(1):数组是分配在一段连续的内存中,在分配空间时,就得确定大小。这样就造成了空间利用率不高,对数据长度的扩展不方便。
(2):由于数组是分配在一段连续的内存空间中,这也就要求我们在插入或删除元素时,需要移动大量的数据。
链表的优点:
(1):链表的中的内存分配不要求是连续的,因此可以提供存储空间的利用率,另外,由于链表是一个不连续的动态空间,通过指针来连接,因此,数据长度很容易扩展
(2):由于链表前后是存放在一堆不连续的内存空间中,在元素是插入和删除,只需要调整一下元素的指针域的值,就可以很方便的插入或删除一个值。
链表的缺点:
(1):无法元素进行随机的访问,平均的时间复杂度为:O(n)
(2):查找元素的效率低,最好情况为O(1),最坏的情况依旧是:O(n)
因此,如果你存储的数据需要进行大量的查找、访问的,而对于插入和删除的操作比较少的话,最好是选择顺序结构进行存储,
如果你存储的时间需要进行大量的插入或删除的操作,但是对于数据的查找、访问没那么频繁的话,最好是选择链式结构进行存储你的数据
4、线性表的链式结构实现以下函数
(我们这里就采取(单链表)一个指针域非循环的方式来实现以下函数,因为这是链表中最简单的一种形式,我在后续会还会介绍其他几种链表的一些实现方式和对应的使用场景)
线性表的操作包括如下几种
(1) InitList(& L)
//构造一个空的线性表
(2) DestroyList(& L)
//线性表存在了,消耗一个线性表
(3) ClearList(&L )
//清空线性表中的内容
(4) ListEmpty(L)
//判断线性表是否为空
(5) ListLength(L)
//返回线性表的长度
(6) GetElem(L,i,& e)
//返回线性表i位置上的元素值,通过e返回
(7) PriorElem(L,cur_e,&pre_e)
//如果cur_e是线性表中的元素,而且不是第一个,那么我们就可以返回该元素前一个元素的值
(8) NextElem(L,cur_e,&next_e)
//如果cur_e是线性表中的元素,而且不是最后一个,就返回它下一个元素的值
(9) Listinsert(&L,i,e)
//如果线性表存在了,而且i符合条件,则在i位置插入一个元素
(10)ListDelete(&L,i)
//删除i位置上的元素
(11) ListDelete_data(&L,e,order)
//删除指定的元素e,order决定是删除一个,还是全部。
(12) Connect_two_List(L_a,L_b,& L_c)
//连接两个线性表,除去重复的内容
(13)print(L)
//打印线性表
5、线性表链式表示实现—元素结构
typedef int Elemtype;
/链式结构,我们打算在链表中添加一个
//保存长度的头结点,加入这个结点可以方便我们对结点做一些
//基本的操作,结点保存的是线性表的长度
struct Node
{
//结点的值,如果是头结点,保存是链表的长度
Elemtype value;
//下一个结点的地址
Node * next;
};
6、线性表链式表示实现–InitList函数
//创建一个空链表,每个头结点就代表一个链表
void InitList(Node * & head) {
head = new Node();
head->value = 0;
head->next = NULL;
}
7、线性表链式表示实现–DestroyList函数
//销毁一个链表
void DestroyList(Node * & head) {
delete head;
head = NULL;
}
8、线性表链式表示实现–ClearList函数
//清空整个列表
void ClearList(Node * & head) {
head->value = 0;
head->next = NULL;
}
9、线性表链式表示实现–ListEmpty函数
//判断链表是否为空。空的话就返回true
bool ListEmpty(Node * head) {
return head->value == 0;
}
10、线性表链式表示实现–ListLength函数
//返回线性表的长度
bool ListLength(Node * head) {
return head->value;
}
11、线性表链式表示实现–GetElem函数
//得到某个位置上的值
bool getitem(Node * & head, int i, Elemtype & value) {
//我们要求程序返回特定位置上的值
//我们一样是从头结点开始寻找该位置
int j = 0;
Node * L = head;
//想要的那个位置是否合法
if (i<1 || i >head->value)return false;
//同样是先得到前一个结点
while (j < i - 1) {
L = L->next;
++j;
}
value = L->next->value;
return true;
}
我们对链表进元素访问的时候,我们可以考虑一下他的时间复杂度,因为我们每次都要从头结点开始,一个一个的往下去寻找我们需要的那个位置,这样最好情况就是:O(1),但是最坏情况是:O(n),而我们顺序结构表示的时候,它需要的时间是保持在O(1)的
11、线性表链式结构实现–PriorElem函数
//得到某个元素前面的那个元素的值,这个时候我们先写一个判断一个元素是否在
//线性表中,并且保存该元素在线性表中的位置
bool isExit(Node * & L, Elemtype cru_e,int & loc) {
while (L->next) {
//代表当前的位置
++loc;
if (L->next->value == cru_e) {
return true;
}
L = L->next;
}
return false;
}
//返回当前元素的前一个元素
bool PriorElem(Node * head, Elemtype cru_e, Elemtype & pre_e) {
Node * L = head;
int loc = 0;
if (isExit(L, cru_e, loc)) {
//不是线性表中的第一个元素
if (loc != 1)
{
pre_e = L->value;
return true;
}
}
return false;
}
12、线性表链式表示实现–NextElem函数
//得到某个元素的下一个元素的位置
bool NextElem(Node * head, Elemtype cru_e, Elemtype & next_e) {
Node *L = head;
int loc = 0;
if (isExit(L, cru_e, loc)) {
//同样是使用isExit函数来判断元素是否存在,并且得到这个元素的前一个结点,
//如果这个元素的位置不在线性表达最后一个位置的话,
//我们就可以得到下一个位置元素的值
if (loc != head->value) {
next_e = L->next->next->value;
return true;
}
}
return false;
}
13、线性表链式表示实现–Listinsert函数
//插入函数
bool Listinsert(Node * & head,int i, Elemtype value) {
//插入到前面的方法
int j = 0;
Node * L = head;
//如果插入的位置不合法,直接返回错误提示
if (i<1 || i>head->value + 1)return false;
//得到插入位置的前一个结点
while (j < i-1) {
L = L->next;
++j;
}
//s是一个临时结点
Node * s = new Node();
s->value = value; //先对临时结点赋值
s->next = L->next; //让临时结点下一个位置指向当前需要插入前一个结点的下一个位置
L->next = s; //让前一个结点下一个位置指向临时结点,完成
//线性表的长度加一
++head->value;
return true;
}
链表实现元素的插入,它的时间都是花在寻找那个插入位置那里,这里我们不需要对元素进行大量的移动,而是只是改变某些元素的指针域就可以完成对元素的插入。
14、线性表链式表示实现–ListDelete函数
//根据下标的值进行删除
bool ListDelete(Node * & head, int i) {
//s我们先找到需要删除那个结点的前一个结点
int j = 0;
Node * L = head;
//如果位置范围不合法,直接返回错误的结果
if (i<1 || i>head->value)return false;
while (j < i - 1) {
L = L->next;
++j;
}
L->next = L->next->next;
return true;
}
在删除这一块,它的时间还主要消耗在寻找结点的上,但是同样删除的时候所花的时间是可以忽略的。而且在寻找结点的时候,时间最坏也就是:O(n)
14、线性表链式表示实现–ListDelete_data函数
//按照值进行删除,如果标志位n=0,表示删除第一次出现的那个位置的值
//如果n=1.就是要删除所有的该值
bool ListDelete_data(Node * & head,Elemtype value,int n) {
//我们先找到我们需要删除的那个结点的位置,
//然后调用del_by_index函数
int flag = 0;
int j = 0;
Node *L = head;
//遍历整个线性表,删除所有相关结点
while (L->next)
{
L = L->next;
++j;
if (L->value==value) {
//调用依照下标删除结点的方式。
ListDelete(head, j);
//删除了一个,那么当前位置也会往前推一个
--j;
flag = 1;
if (n == 0)
break;
}
}
if (flag == 0)return false;
return true;
}
14、线性表链式表示实现–Connect_two_List函数
//连接两个链表,当然必须保证两个链表是有序的了,这样链接起来菜式正确的
void Connect_two_List(Node * a, Node * b, Node * c) {
//连接链表的长度为两个链表长度和
c->value = a->value + b->value;
int a_value, b_value;
a_value = a->value;
b_value = b->value;
int i = 1;
int j = 1;
int k = 1;
Node * p_a = a->next;
Node * p_b = b->next;
while (i <= a_value && j <= b_value) {
//cout << p_a->value << " " << p_b->value << endl;
if (p_a->value < p_b->value) {
++i;
Listinsert(c, k, p_a->value);
++k;
p_a = p_a->next;
}
else if (p_a->value > p_b->value) {
++j;
Listinsert(c, k, p_b->value);
++k;
p_b = p_b->next;
}
else {
++j;
++i;
Listinsert(c, k, p_b->value);
--c->value;
++k;
p_b = p_b->next;
p_a = p_a->next;
}
}
while (i <= a_value) {
++i;
Listinsert(c, k, p_a->value);
++k;
p_a = p_a->next;
}
while (j <= b_value) {
++j;
Listinsert(c, k, p_b->value);
++k;
p_b = p_b->next;
}
}
OK,到这里,我现在已经把单链表的一些基本操作都已经实现完了,在写一篇博客,我还打算去实现一下双链表和循环链表。
这篇博客只是个人的见解,肯定有错误的,望大家指出来。
整个链式实现的源代码可以去这里下载:点击这里下载