一.写在前面
“生活不止眼前的苟且,还有诗和远方的田野,你赤手空拳的来到人世间,为找到那篇海不顾一切”,高晓松说。我们学习开发这么多年,也明白“开发不止当下的bug,还有将来和未发现的bug,我们在开发的路上不断探索,只为找寻那优质的产品”。开发犹练功,可分为外功招式和内功心法,自计算机问世以来,曾经出现了好多外功招式(编程语言)以及内功心法(数据结构)。各大外功招式都曾名噪一时,有被时间遗忘的,有曾昙花一现的,也有经久不衰的。语言总体分为汇编语言,机器语言,脚本语言和高级语言。而我们所常见的语言有C、C++、C#、Java、python、php、易语言等等,不管是像C和Java这类编译型语言还是像python这类解释型语言,他们都有一个相通的东西,那就是编程思想。语言只是工具,思想才是精髓。而在编程兴起的长河中,有一套基本适用于大部分语言的内功心法——数据结构。本篇文章以线性表(List)开始说起。
二.基本概念
-
基本定义
线性表(List):零个或多个数据元素的有限序列。
-
关键点
1.首先线性表是一个序列:元素之间有确定的顺序;
2.线性表是有限的:元素的个数n为线性表的长度,当n为0时,记为空表。
三. 线性表的存储结构
顺序存储结构
1. 基本知识
-
用一段地址连续的存储单元依次存储线性表的数据元素
线性表(a1,a2,……an)的顺序存储示意图如下:
顺序存储结构简单说就是在内存中开辟了一块内存,按照依次占位的方式存入元素。
2.插入与删除
获取操作:线性存储结构获取元素非常简单,我们只需要传入我们想获取的第几个元素,线性表就会根据下标找到这个元 素。
插入操作 :
插入操作的思路如下:
- 如果插入位置不合理,则抛出异常;
- 如果线性表长度大于等于数组长度,则抛出异常或者动态增加容量;
- 从最后一个元素开始向遍历到第i个位置,分别对它们都向后移动一个位置;
- 将要插入的元素填入到第i个位置;
- 表长加1。
删除操作:
删除操作的思路如下:
- 如果删除位置不合理,则抛出异常;
- 取出删除元素;
- 从删除元素位置开始遍历到最后一个元素位置,分别将它们向前移动一个位置;
- 表长减1。
线性表的顺序存储结构,在存、读数据元素时,它的时间复杂度为O(1),插入和山删除时,时间复杂读度为O(n)。因此这种存储方式适合元素个数不太变换,更多的操作是存读数据的应用。
3.优缺点
优点 | 缺点 |
---|---|
|
|
-
线性表的链式存储结构
前面已经提到,在线性表的顺序存储结构中,当对表中元素进行插入与删除操作时需要改动大量的
元素来完成操作,因而容易耗费大量的时间。因此我们追寻一种能提升工作效率的方法。
1.基本知识
在线性表的链式存储结构中,每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系除了存储其本身的信息之外,还需要存存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称为指针或链。这两部分信息组成数据元素ai的存映射,称为结点(Node)。
n个结点链成一个链表,即为线性表的链式存储结构,因为此链表的每个结点只包含一个指针域,所以叫单链表。如下图所示:
我们把链表中第一个结点的存储位置叫做头指针。
头指针与头结点的区别:
头指针 | 头结点 |
|
|
2.获取、插入与删除
在线性链表的存储结构中,我们要计算任何一个元素是很容易的,但在链式存储方式中,无法知道第i个元素在哪里,必须从头开始查找。
获取第i个元素的思路:
- 声明一个节点p指向链表的第一个节点,初始化j从开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一节点,j累加1;
- 若到链表末尾p为空,则说明第i个元素不存在;
- 否则查找成功,返回节点p的数据。
在进行单链表的插入时,假设存储元素e的结点为s,要实现结点p、p->next和s之间逻辑关系的变化,只需要
将结点s插入到结点p和p->next之间即可。其他结点无需更改。
单链表第i个数据插入结点的算法思路:
- 声明一结点p指向链表的第一个结点,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
- 若到链表末尾p为空,则说明第i个元素不存在
- 否则查找成功,在系统中生成一个空结点s
- 将数据元素e赋值给s->data
- 单链表的插入标准语句为s->next=p->next;p->next = s
- 返回成功
在进行单链表删除时,设存储元素ai的结点为q,要实现将结点q删除操作,其实就是将它的前继结点的指针绕过,指向它的后继结点即可。如下图:
单链表第i个数据删除思路:
- 声明结点p指向链表第一个结点,初始化j从1开始
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一节点,j累加1
- 若到链表末尾p为空,则说明第i个元素不存在
- 否则查找成功,将欲删除的结点p->next赋值给q
- 单链表的删除标准语句p->next=q->next
- 将q结点中的数据赋值给e,作为返回
- 释放q结点
- 返回成功
我们发现单链表的插入和删除操作,它们都有两部分组成:第一部分是遍历查找第i个元素;第二部分是插入和删除元素。它们的时间复杂度都是O(n)。很显然对于插入或者删除数据比较频繁的操作时,单链表的效率明显高于顺序方式存储。
-
单链表结构与顺序存储结构比较
单链表结构与顺序存储结构比较
存储分配方式 时间性能 空间性能 查找 插入和删除 顺序存储结构 连续的一段存储单元依次存储 O(1) 需要平均移动表长一半的元素,时间为O(n) 需要预分配存储空间,分大了,浪费,分小了,溢出 链式存储结构 一组任意的存储单元存放 O(n) 在找出某位置指针后,时间为O(1) 不需要预分配,只要有就分配,元素个数不受限
在C/C++语言阵营里因为具有指针的能力,可以很方便的进行链表的操作,而在java,C#阵营里,虽然不使用指针,但因为启用了引用机制,从某种角度也间接实现了指针的一些作用,而像早起的Basic,Fortran由于没有指针,它们用数组来代替,通常我们把这种用数组描述的链表叫做静态链表。
-
循环链表
对于单链表而言,由于每个结点只存储了向后的指针,到了尾标志就停止了。如果我们将单链表的末尾结点的指针端由空指针改为指向头结点,那么就使整个单链表形成一个环,生生不息,无尽无穷,这种头尾相连的单链表称为单循环链表,简称循环链表(circular linked list)。如图:
-
双向链表
链表中的结点只包含一个指向后继的指针我们称为单链表,当我们在设置一个指向前驱结点的指针域,它就成了双向链表。所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。如图:
三.实例代码
话不多说直接上代码:
#include<stdlib.h> #include <iostream> using namespace std; //首先定义一个结点 struct Node { int data; Node* next; }; //定义一个链表类 class ListLink { public: ListLink(); ~ListLink(); public: bool clearList(); inline bool isEmpty() { return m_head == NULL; } int Length(); bool GetElem(const int index, int *data); //获取元素 bool Insert(const int index, int data); //插入元素 bool Delete(const int index, int *data); //删除元素 private: Node* m_head; }; ListLink::ListLink() :m_head(NULL) { } ListLink::~ListLink() { Node *p = m_head; while (m_head) { p = m_head; m_head = m_head->next; delete(p); } } bool ListLink::clearList() { Node *p = m_head; while (m_head) { p = m_head; m_head = m_head->next; delete(p); } return true; } int ListLink::Length() { Node *p = m_head; int len = 0; while (p != NULL) { len++; p = p->next; } return len; } bool ListLink::GetElem(const int index, int *data) { Node *p = m_head; int j = 0; while (p&&j < index) { p = p->next; j++; } if (p == nullptr) { return false; } *data = p->data; return true; } bool ListLink::Insert(const int index, int data) { Node *p = m_head; Node *s; int j = 0; if (index == 0) { s = (Node *)new Node[1]; s->data = data; s->next = p; m_head = s; return true; } while (p&&j < index - 1) { p = p->next; j++; } if (p == NULL) { return false;//到队尾了 } s= (Node *)new Node[1]; s->data = data; s->next = p->next; p->next = s; return true; } bool ListLink::Delete(const int index, int *data) { Node *p = m_head; Node *s; if (p == NULL) { return false; } int j = 0; if (index == 0) { m_head = m_head->next; *data = p->data; delete p; p = NULL; return true; } while (p&&j < index - 1) { j++; p = p->next; } if (p == NULL) return false; s = p->next; p->next = p->next->next; *data = s->data; delete s; s = NULL; return true; } //主函数测试 int _tmain(int argc, _TCHAR* argv[]) { int a = 0; int *p = &a; ListLink list; //插入测试 list.Insert(0, 1); list.Insert(1, 2); list.Insert(2, 3); list.Insert(3, 4); list.Insert(3, 5); list.Insert(1, 6); //链表长度 cout <<"链表长度:="<< list.Length()<<endl; //插入的元素值 cout << "各个元素的值依次是:"<< endl; for (int i = 0;i < list.Length();i++)//遍历该链表 { if (list.GetElem(i, p)) { cout << *p<<endl; } } cout << endl; //删除元素 int e = 3; list.Delete(2,&e); //链表长度 cout <<"删除元素后链表长度:="<< list.Length()<<endl; //删除元素之后剩下的元素 cout << "删除元素之后剩下各个元素的值依次是:"<< endl; for (int i = 0;i < list.Length();i++)//遍历该链表 { if (list.GetElem(i, p)) { cout << *p<<endl; } } cout << endl; //清空链表 list.clearList(); //链表长度 cout <<"清空后链表长度:="<< list.Length()<<endl; system("pause"); }
程序输出测试结果如下:
}
【下一篇:】数据结构浅浅析之(二)——栈和队列(Stack && Queue):https://blog.csdn.net/weixin_39951988/article/details/86518425