一.概念
线性表:由零个或多个数据元素组成的有限序列称为线性表。
关键点:1)元素组成了一个序列,也就是元素之间是有顺序的;
2)多个元素组成的线性表中,第一个元素没有前驱,最后一个元素没有后继,其他元素都有一个前驱和一个后继,也就是说线性表首尾相接后不再是线性表;
3)线性表元素个数一定是有限的,计算机无法处理无限个元素的线性表;
4)存在元素个数为0的线性表,称为空表。
数据类型:一组性质相同的值的集合及定义在此集合上的一些操作的总称。
抽象数据类型:Abstract Data Type(ADT),指一个数学模型及定义在该模型上的一组操作。抽象数据类型的定义仅取决于它的一组逻辑特性,和其在计算机内部的表示和实现无关(数据类型已经抽象概括出来了,没有必要再关注实现细节)。抽象数据类型可以使用以下的格式作为标准格式进行描述:
二.线性表
1.顺序存储的线性表
在内存中,数组这种数据结构就是顺序存储的,它在内存中会开辟一段连续的内存,内存地址自然也是连续的整数。可以理解为什么静态数组声明时必须指定数组长度了,因为需要指定数组占用内存的长度否则数组可能会覆盖其他对象的数据。同时,既然顺序存储的线性表占用的内存的地址是连续的整数,因此可以使用等差数列的方式求出第i个数据的内存地址:LOC(ai) = LOC(a1) + (i-1)*c,其中LOC是求内存地址的函数,c是一个数据元素占用内存长度,这个公式是说第i个元素的内存地址等于第一个元素的内存地址加上每个元素占用内存长度乘以i-1。
一些操作的时间复杂度:
1)查找操作:只需要求得元素的内存地址再获得数据即可,因此时间复杂度为O(1)。
2)插入操作:除了将数据存储到表中,还需要将表中的元素依次挪动。最坏情况挪动元素n个(插入表的开始位置),最好情况挪动元素0个(插入表的最后),平均情况挪动元素n/2个,因此时间复杂度o(n)。
3)删除操作:删除操作和插入操作类似,在删除过程中也需要依次挪动被删除元素后面的元素,因此时间复杂度为O(n)。
顺序存储结构的优缺点:优点1)无须为表示表中元素之间的逻辑关系增加额外的存储空间;优点2)快速地存取表中任意位置地元素;缺点1)插入和删除操作需要移动大量元素;缺点2)线性表长度变化较大时,难以确定存储空间地容量;缺点3)容易产生碎片化的存储空间。
2.链式存储的线性表
存储线性表时,相邻元素所在的内存不是相邻的,而是采用存储下一个内存地址指针的方式将整个线性表存储起来,这种方式就是链式存储。存储数据元素信息的域称为数据域,存储直接后继位置的域称为指针域,指针域中存储的信息称为指针或链,指针域和数据域可以合成为存储映像或节点。链表中第一个节点称为头节点,最后一个节点的指针存储的下一个节点地址为null。链表常见单链表和双链表,单链表指针域只存储下一个节点的指针,双链表指针域同时存储上一个节点和下一个节点的指针。
头指针:指向链表第一个节点的指针,头指针具有标识作用,常用头指针冠以链表的名字,不论链表是否为空,头指针均不为空;头节点:放在第一个元素节点前的节点,数据域一般无意义(也可以存放链表长度),头节点不是链表的必须要素。简要总结:头指针指向头节点,头节点指向第一个元素。
一些操作的时间复杂度:查找操作最好情况不用遍历,时间复杂度O(1),最坏情况需要遍历,时间复杂度O(n);插入删除操作只需要修改当前节点和上下游节点的指针即可,但是第一次在某个位置插入删除需要先计算指针位置(查找操作),时间复杂度O(n),计算出位置后进行插入和删除操作的时间复杂度O(1)。
3.静态链表
申请一个足够大的顺序存储空间(数组)用来存放数据,数据类型为链式存储的线性表中的节点,节点中的指针地址使用数组下标代替(存储下一个节点的下标,称为游标),这样的数据类型就是一个静态链表。静态链表的增删查改的方法和链式存储的线性表相似,但是实际的数据是存储在特定空间中的,不像链表散落在各处。
4.循环链表
将单向链表的尾节点指向头节点,这个单向链表就变成了一个环,于是就构成了一个循环链表。
约瑟夫问题:据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领桥塔帕斯后,39个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3个人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus和他的朋友并不想遵从,Josephus要他的朋友先假装遵从,他将朋友和自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。
现在使用C#程序模拟约瑟夫问题并输出编号的自杀顺序。
namespace JosephusQuestion { /// <summary> /// 节点数据 /// </summary> class Node { //下一个节点 public Node next; //数据内容 public int data; /// <summary> /// 构造函数 /// </summary> /// <param name="data">数据内容</param> public Node(int data) { this.data = data; } } /// <summary> /// 循环链表 /// </summary> class LoopedLinkedList { //头节点 public Node head; //尾节点 public Node tail; //节点数 public int count; /// <summary> /// 构造函数 /// </summary> /// <param name="count">给定的节点数</param> public LoopedLinkedList(int count) { //给定节点数小于0,不合规范,直接返回(可以添加提醒不合规的代码或报错) if (count < 0) return; //节点数大于或等于0,设置节点数 this.count = count; //节点数大于0时需要创建节点并形成链表 if(count > 0) { //创建第一个头节点 head = new Node(1); //记录当前访问到的节点位置 Node temp = head; //遍历创建剩余的节点 for (int i = 0; i < count - 1; i++) { temp.next = new Node(i + 2); temp = temp.next; } //尾节点赋值 tail = temp; //尾节点指向头节点 tail.next = head; } } /// <summary> /// 移除存储指定数据的节点 /// </summary> /// <param name="data">数据</param> /// <returns>是否成功移除</returns> public bool RemoveNode(int data) { //没有任何节点直接返回 if (count == 0) return false; //上一个访问的节点 Node last = tail; //当前访问的节点 Node current = head; //遍历所有节点,因为是循环节点,这里采用从0到count-1的方式循环 //如果采用判断current.next是否为空的方式循环,会陷入死循环 for (int i = 0; i < count; i++) { //判断当前节点是否是要删除的节点 if(current.data != data) { last = current; current = current.next; } else { return RealRemoveNode(last, current); } } return false; } /// <summary> /// 移除指定下标位置的节点 /// </summary> /// <param name="index">指定下标</param> /// <returns>是否成功移除</returns> public bool RemoveNodeAt(int index) { //校验给定的下标合法性 if(index > count - 1 || index < 0) { return false; } //遍历找到要移除的节点 Node last = tail; Node current = head; for(int i = 0;i < index; i++) { last = current; current = current.next; } //调用移除函数移除 return RealRemoveNode(last, current); } /// <summary> /// 真正用来移除节点的函数,私有的 /// </summary> /// <param name="last">上一个节点</param> /// <param name="current">当前要移除的节点</param> /// <returns></returns> private bool RealRemoveNode(Node last,Node current) { //校验参数是否合法 if (last == null || current == null) return false; //判断是否只剩余一个节点 if (count == 1) { head = null; tail = null; current.next = null; } //判断当前要删除的节点是否是头节点或尾节点,头尾节点的删除方式和中间节点不同 else if (current == head) { head = current.next; tail.next = head; current.next = null; } else if (current == tail) { tail = last; tail.next = head; current.next = null; } else { last.next = current.next; current.next = null; } //删除节点后将节点数--并返回 count--; return true; } /// <summary> /// 实现约瑟夫问题的函数,在其中会打印信息并按照约瑟夫问题的规则移除所有节点 /// </summary> public void JosephusQuestionRemove() { //校验当前节点数是否为空 if (this.count == 0) return; //用于计数 int count = 1; //记录当前节点和上一个节点 Node last = tail; Node current = head; //死循环不断移除,直到节点数为0 while(this.count > 0) { if (count == 3) { Node temp = current.next; Console.Write(current.data + "->"); RealRemoveNode(last, current); current = temp; count = 1; } else { last = current; current = current.next; count++; } } } } class Program { static void Main(string[] args) { LoopedLinkedList lll = new LoopedLinkedList(41); lll.JosephusQuestionRemove(); Console.ReadKey(); } } }
5.判断单链表中是否有环
方法一:使用p、q两个指针,p每次向前走一步;q每次从头向前走n步,直到两个指针指向同一个节点。如果是没有环的单链表,每次p走的总步数和q走的步数应该相同;如果是有环的单链表,在环的位置p的步数和q的步数不相同,当前p或q指向的节点就是成环的节点。这个方法的时间复杂度是O(n)。
方法二:同样使用p、q两个指针,p每次走一步,q每次走两步,如果是有环的单链表,则一定会有某个时刻p和q指向同一个节点。这个方法的时间复杂度是O(1)。
6.双向链表
双向链表的节点中指针地址包含两个指针,同时记录上一个节点地址和下一个节点地址。
三.栈和队列
1.栈
栈是后进先出的数据结构,是线性表的一种具体形式。
2.队列
队列是先进先出的数据结构,也是线性表的一种具体形式。