1、什么是链表
链表是一种用于存储数据集合的数据结构。链表有一下的属性:
- 相连的元素之间通过指针进行连接
- 最后一个元素的后继指针为NULL
- 在执行的过程中链表的长度可以增加或者是减少
- 链表的空间可以按需进行分配
- 内有内存空间的浪费,但是链表中的指针需要额外的内存开销
2、链表的抽象数据类型
链表的主要操作:
- 插入:插入一个元素到链表中
- 删除:移除并返回链表中指定位置的元素
链表的辅助操作
- 删除链表:移除链表中的所有的元素
- 计数:返回链表中元素的个数
- 查找:寻找从链表表尾开始的第n个结点
3、为什么需要使用链表
有许多数据结构可以像链表一样做同样的事情。在讨论链表前,首先要了解链表和数组的区别,链表和数组都是可以用于存储数据集合的。由于两者的用途相同,所以需要对他们的的用法进去区别,需要了解什么情况下使用数组,什么情况下使用链表;
4、数组的概述
整个数组在内存中分配一块连续的存储空间。通过使用特定的元素的索引作为数组下标,可以在常数时间内访问数组元素。
数组的特点是:
- 访问速度快(常数时间)
- 简单易用
数组访问的缺点:
- 大小固定:数组的大小是静态的(使用前指定数组的大小)
- 分配一个连续的空间:数组初始化分配空间时,有时无法分配能存储整个数组的内存空间(数组的规模太大)
- 基于位置的插入操作复杂:如果在数组中指定的位置插入元素,则需要移动数组中其他的元素的位置
5、链表的优点
链表的优点是:他们可以在常数时间内扩展。当创建数组的时候,必须分配能存储一定数量的内存,如果向数组中添加跟多的元素,那么必须创建一个新的数组,然后把原数组中的元素复制到新数组中,这将花费大量的时间;
当然,可以通过为数组预先分配一个很大的空间来防止上述的情况的发生。但是这个方法会因为分配超过用户需要的空间而造成内存的浪费。而对于链表,初始时仅需要分配一个元素的存储空间,并且添加新的元素也很容易,不需要做任何内存复制和重新分配的操作
6、链表的缺点
主要体现在访问单个元素的时间开销问题。数组是随机存取的,即存取数组中任意元素的时间开销为O(1)。而链表在最差情况下访问一个元素的开销为O(n)。数组在存取时间方面的另外一个优点是内存的空间局部性。由于数组被定义为连续的内存块,所以任何数组元素与邻居是物理相连的。这极大的得益于CPU的缓存模式。
尽管链表的动态存储分配存储空间有很大的优势,但在存储和检索数据的开销方面却有很大的不足 。有时很难对链表进行操作。如果要删除最后一项,倒数第二项必须更改后继指针值为NULL,这时需要从头开始遍历,找到倒数第二个结点的链接,并设置其后继指针为NULL
7、单链表
链表通常是指向单链表,它包含多个结点,每个结点有一个指向后继元素的next(下一个)指针。表中的最后一个结点的next指针为NULL,表示该链表的结束。
下面是一个链表的声明:
**
- @Author: River
- @Date:Created in 16:42 2018/5/28
- @Description:
*/
public class ListNode {
private int data;
private ListNode next;
public ListNode(int data) {
this.data=data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public ListNode getNext() {
return next;
}
public void setNext(ListNode next) {
this.next = next;
}
}
7.1、链表的基本操作
- 遍历链表
- 插入元素
- 删除元素
7.2、链表的遍历
假设表头结点的指针指向链表中的第一个结点。遍历链表需要完成以下步骤。
- 沿指针遍历
- 遍历是显示结点的内容(或计数)
- 当next指针的值为NULL时结束遍历。
ListLength()函数的输入为链表,其功能是统计链表中结点的个数。下面是函数的代码,还可以添加额外的输出函数来输出链表中的数据
//统计结点的个数
int ListLength(ListNode headNode) {
int length=0;
ListNode currentNode=headNode;
while (currentNode!=null) {
length++;
currentNode=currentNode.getNext();
}
return length;
}
时间复杂度为O(n),用于扫描长度为n的链表。空间复杂度为O(1),仅仅用于创建临时变量
7.3、单链表的插入操作
- 在表头插入一个新结点
- 在表尾插入一个新结点
- 在链表的中间插入一个结点
7.3.1、在单链表的开头插入结点
若需要在表头插入结点,值需要修改一个next指针(新结点的next指针),可以通过以下的两个步骤:
- 更新新结点的next指针使其指向头结点
- 更新表头指针的值,使其指向新结点
7.3.2、在单链表的结尾插入结点
- 新节点的next指针指向null
- 最后一个结点的next指针指向新结点
7.3.3、在单链表的中间插入结点
如果要在位置3增加一个元素,则需要将指针定位于链表的位置2处。即需要从表头开始经过两个结点,然后插入新的结点
位置结点的next指针指向新结点
7.3.4、插入代码如下
ListNode insertLinkedList(ListNode headNode,ListNode nodeToInsert,int position) {
if (headNode == null) {
return nodeToInsert;
}
int size = ListLength(headNode);
if (position > size + 1 || position < 1) {
System.out.println("position of Node to insert is invalid,The valid inputs are 1 to " + (size + 1));
return headNode;
}
if (position == 1){//在表头插入
nodeToInsert.setNext(null);
headNode=nodeToInsert;
return headNode;
}else {
//在表的中间或者末尾
ListNode previousNode = headNode;
int count=1;
while (count < position - 1) {
previousNode = previousNode.getNext();//找到需要插入位置的前一个结点,也就是位置结点
count++;
}
ListNode currentNode = previousNode.getNext();//这个结点是插入位置的结点,如果position=size+1,则currentNode=null
nodeToInsert.setNext(currentNode);
previousNode.setNext(nodeToInsert);
}
return headNode;
}
7.4、单链表的删除
- 在表头删除一个结点
- 在表尾删除一个结点
- 在链表的中间删除一个结点
7.4.1、删除单链表的第一个结点
- 创建一个临时结点,它指向表头指针指向的结点
- 修改表头指针的值,使其指向下一个结点,并移除临时结点
7.4.2、删除单链表的最后一个结点
-遍历链表,在遍历时还要保存前驱结点的地址。当遍历到链表的表尾时,将有两个指针,分别保存表尾结点的指针tail(表尾)及指向表尾结点的前驱结点的指针
- 将表尾的前驱结点的next指针更新为NULL
- 移除表尾结点
7.4.3、删除单链表的中间一个结点
- 与上一种情况类似,在遍历时保存前驱(前一次经过的)结点的地址,一旦找到要被删除的结点,将前驱结点的next指针的值更新为被删除的结点的next值
- 移除需要删除的结点
7.4.4、删除代码如下
ListNode DeleteNodeFromLinkedList(ListNode headNode,int position){
int size=ListLength(headNode);
if (position > size || position < 1) {
System.out.println("position of Node to delete is invalid,the valid inputs are 1 to " + (size));
}
if (position == 1) {
ListNode currentNode = headNode.getNext();
headNode = currentNode;
return headNode;
} else {//删除中间或者是表尾结点
ListNode previousNode=headNode;
int count=1;
while (count < position-1) {
previousNode = previousNode.getNext();
count++;
}
ListNode currentNode = previousNode.getNext();
previousNode.setNext(currentNode.getNext());
currentNode=null;
}
return headNode;
}
7.5、清空单向链表
void DeleteLinkedList(ListNode head) {
ListNode auxilaryNode,iterator = head;
while (iterator != null) {
auxilaryNode = iterator.getNext();
iterator = null; //在java中垃圾回收器将自动处理
iterator =auxilaryNode;
}
}
7.6、单向链表总结
单向链表是最常用也是基本的链表结构,需要熟练的掌握单向链表的增删改查等基本的操作