数据结构浅浅析之(一)——线性表(List 附C++代码)

一.写在前面

“生活不止眼前的苟且,还有诗和远方的田野,你赤手空拳的来到人世间,为找到那篇海不顾一切”,高晓松说。我们学习开发这么多年,也明白“开发不止当下的bug,还有将来和未发现的bug,我们在开发的路上不断探索,只为找寻那优质的产品”。开发犹练功,可分为外功招式和内功心法,自计算机问世以来,曾经出现了好多外功招式(编程语言)以及内功心法(数据结构)。各大外功招式都曾名噪一时,有被时间遗忘的,有曾昙花一现的,也有经久不衰的。语言总体分为汇编语言,机器语言,脚本语言和高级语言。而我们所常见的语言有C、C++、C#、Java、python、php、易语言等等,不管是像C和Java这类编译型语言还是像python这类解释型语言,他们都有一个相通的东西,那就是编程思想。语言只是工具,思想才是精髓。而在编程兴起的长河中,有一套基本适用于大部分语言的内功心法——数据结构。本篇文章以线性表(List)开始说起

二.基本概念

  • 基本定义

      线性表(List):零个或多个数据元素的有限序列。

  •  关键点

1.首先线性表是一个序列:元素之间有确定的顺序;

2.线性表是有限的:元素的个数n为线性表的长度,当n为0时,记为空表。

三. 线性表的存储结构

  顺序存储结构

       1. 基本知识   

  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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尘海折柳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值