线性表的定义:零个或多个数据元素的有限序列
线性表元素的个数n(n>=0)定义为**线性表的长度,**当n=0时,称为空表
线性表的中的元素的相邻的前一个元素称为直接前驱元素,后一个元素称为直接后继元素
定义:线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素
线性表长度:线性表中数据元素的个数
数组长度:存放线性表的存储空间的长度
通过数组的下标直接获得
int[] array = new int[] { 1,2,3,4,5 }; Console.WriteLine(array[1]); Console.ReadLine();
更改元素,直接通过数组下标直接修改就行了
int[] array = new int[] { 1,2,3,4,5 }; array[1] = 3; Console.WriteLine(array[1]); Console.ReadLine();
插入元素有三种方式
尾部插入
对于顺序表来说,如果插入元素在数组尾部,那么传入的下标参数index等于size。比如说:一个只有5个元素的顺序表,在尾部插入就是相当于添加一个下标等于原来顺序表的size的元素
这里的size为线性表中实际拥有的元素个数
如果插入元素在数组的中间或者头部,那么index一定会小于size
中间插入
class SeqList { private int[] array; private int size; public SeqList(int MaxN) { array = new int[MaxN]; size = MaxN; } public void insert(int index, int element) { if (index < 0 || index > size) { throw new IndexOutOfRangeException("超出数组实际元素范围!"); } for (int i = size - 1; i <= index; i--) { array[i + 1] = array[i]; } array[index] = element; size++; } public void print() { for (int i = 0; i < array.Length; i++) { Console.WriteLine(array[i]); } } } class Program { static void Main(string[] args) { SeqList seqList = new SeqList(10); seqList.insert(0, 1); seqList.insert(1, 2); seqList.insert(2, 3); seqList.insert(3, 4); seqList.insert(4, 5); seqList.print(); Console.ReadLine(); } }
如果传入的下标参数index大于size或小于0,则认为是非法输入,会直接抛出异常。
超范围插入
超范围插入就是在已经装满元素的线性表之中,再插入一个新元素。
这里就要涉及扩容
扩容的方法
public void resize() { int[] arrayNew = new int[array.Length * 2]; System.Array.Copy(array, 0, arrayNew, 0, array.Length); array = arrayNew; } if (size >= array.Length) { resize(); }
扩容的本质:创建一个新数组,然后将旧数组中的元素复制到新的数组当中
一般情况之下,是不会使用扩容的。遇到数组满的情况下,直接判断为false。
插入算法的本质
1、传入一个下标和元素
2、从线性表的后面向前遍历
3、以传入的下标为极限进行限制
4、然后以相邻的大的数取代前一个数,以完成空出插入位置的目的
5、最后将元素赋值给这个空位置
public void delete(int index) { if (index < 0 || index >= size) { throw new IndexOutOfRangeException("超出数组实际元素范围"); } for (int i = index; i < size - 1; i++) { array[i] = array[i + 1]; } size--; }
删除的本质就是传入下标,从该下标遍历完数组,将后一个元素覆盖前一个元素,直到null赋值给最后一个元素
顺序表的优劣
对于元素的查找,顺序表的时间复杂度为O(1)
对于元素的删除与插入,顺序表的时间复杂度为O(n)
定义:对于数据元素来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息。
将存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素的存储映像,称为结点(Node)。
n个结点链结成一个链表,即为线性表的链式存储结构,叫做单链表。
特点:用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的
C#中顺序表为List<>,链表为LiskedList<>
在C#当中标准库中标的链表是双向带环、首尾相接的链表
链表的分类
C语言当中链表分为:单链表、静态链表、循环链表、双向链表。
链表分类的标准:
1、按结点方向:单链表,双向链表
2、按照是否循环:普通链表、循环链表
3、按照是否带头结点
4、按照头结点在链表表首:头结点在链表表首,头结点在链表表尾。
链表的使用
在这里C语言当中,使用结构和指针来描述链表。而C#使用类和引用来描述链表
对于C#来说,链表直接可以调用类库当中的。而C语言没有相应的类库,只能自己手撸。
链表的实例化与调用方法
LinkedList<int> ll = new LinkedList<int>();//实例化一个链表,这个链表是C#类库中自带的,是双向链表 LinkedListNode<int> node0= ll.AddFirst(1);//添加链表的头结点,LinkedListNode<int>是链表结点的类型 LinkedListNode<int> node1 = ll.AddAfter(node0, 1);//在node0的后面添加一个结点 LinkedListNode<int> node2 = ll.AddLast(4);//添加链表的尾结点 LinkedListNode<int> node4 = ll.AddLast(4);//添加链表的尾结点 LinkedListNode<int> node3 = ll.AddLast(2);//添加node4的前一个结点 ll.Remove(node3);//删除node3结点
链表的遍历
foreach (int nodeData in ll)//遍历链表 { Console.WriteLine(nodeData); } for (LinkedListNode<int> node = ll.First; node != null; node = node.Next) { Console.WriteLine(node.Value); }
C#直接调用链表很方便,但是在大多数的情况之下,需要自己写出链表
手撸版链表的定义
class Node { public string data;//数据域 public Node next;//指针域 } class Program { static void Main(string[] args) { Node n0 = new Node();//实例一个结点 n0.data = "a";//给结点的数据赋值 Node n1 = new Node(); n1.data = "b"; Node n2 = new Node(); n2.data = "c"; n0.next = n1;//将n0指针指向n1 n1.next = n2;//将n1指针指向n2 Node head = n0;//head为头结点 Console.WriteLine(head.next.next.data);//这是指向n2 for (Node node = head; node != null; node = node.next) { Console.WriteLine(node.data); } Console.ReadLine(); }
一个链表只有一个头结点的引用(head)
链表的使用步骤
1、新建结点
2、将他们串起来
链表遍历的原理:生成一个动态指针p,在链表中结点不为空,一直向后遍历。
我们无法给链表每个元素起一个变量名,所以设置一个头结点,该结点永远指向链表首元素,这样的链表才有意义。
链表的插入和删除在进行时需要进行查找元素的过程,因此时间复杂度为O(n),但是如果只考虑到插入与删除操作,时间复杂度都是O(1)。
链表的插入方法
链表的创建操作,实际上就是往链表里面插入新的链表结点
插入操作可以分为以下几种情况
1、插入位置为0(不管头结点是否为空)
2、插入位置在链表的中部或末尾
3、插入位置无效(<0,或者在末尾值后):静止操作,返回false
头插法
if (index == 0)//头插法 { insertedNode.next = head; head = insertedNode; }
尾插法
if (index == size)//尾插法 { last.next = insertedNode; last = insertedNode; }
中间插入
int cnt = 0; for (Node p = head; p != null; p = p.next) { if (cnt == index - 1) { insertedNode.next = p.next; p.next = insertedNode; return; } }
链表的删除
if (index == 0)//删除头结点 { head = head.next; }
在这里插入代码片
int cnt = 0;//删除中间 for (Node p = head; p != null; p = p.next) { if (cnt == index-1) { p.next = p.next.next; return; } } cnt++;
int cnt = 0; for (Node p = head; p != null; p = p.next) { if (cnt ==index-1 ) { p.next = null; return; } cnt++;
顺序表与链表的比较
随机访问:顺序表可以直接定位一条语句内完成
链表顺序访问下一个元素
插入、删除:顺序表需要移动大量元素,且移动次数与元素个数,元素插入删除的位置关系都较大
链表在固定的常数时间内可以完成
内存使用:顺序表固定大小的一块连接内存块,如果需要扩容则需要拷贝大量元素
链表容易造成内存碎片
头指针与头结点的异同
头指针:
1、头指针是指链表指向第一个节点的指针,若链表有头·节点,则是指向头结点的指针
2、头指针具有标识作业,所以常用头指针冠以链表的名字
3、无论链表是否为空,头指针均不为空。头指针是链表的必要元素
头结点
1、头结点是为了操作的统一和方便设立的,放在第一元素的结点之前,其数据域一般无意义
2、有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了
3、头结点不一定是链表必须要素