什么是链表
链表是一种用于存储数据集合的数据结构。链表有以下属性:
- 相邻元素之间通过指针连接
- 最后一个元素的后继指针值为null
- 在程序执行过程中,链表的长度可以增加或者缩小
- 链表的空间能够按需分配(直到系统内存耗尽)
- 没有内存空间的浪费(但是链表中的指针需要一些额外的内存开销)
链表抽象数据类型
链表抽象数据类型中的操作如下:
链表的主要操作
- 插入:插入元素到链表中
- 删除:移除并返回链表中指定位置的元素
链表的辅助操作: - 删除链表:移除链表中的所以元素(清空链表)
- 计数:返回链表中元素的个数
- 查找:寻找从链表表尾开始的第n个结点(node)
链表 VS 数组
数组:
- 整个数组所有的元素都存储在操作系统分配的一个内存块中。
- 通过使用特定元素的索引作为数组下标,可以在常数时间内访问数组元素。
数组优点:
- 简单且易用
- 访问元素块(常数时间)
数组的缺点:
- 大小固定:数组的大小是静态的(在使用前指定数组的大小)
- 分配一个连续空间块:数组初始分配空间时,有时无法分配能存储整个数组的内存空间(当数组规模太大时)
- 基于位置的插入操作实现复杂:如果要在数组中的给定位置插入元素,可能需要移动存储在数组中的其他元素,这样才能腾出指定的位置来放插入的新元素。如果在数组的开始位置插入元素,那么移动操作的开销将更大。
动态数组: 了解这种概念
动态数组是一种可随机存取且可自动调整大小的线性数据结构,能够添加或删除元素。
实现动态数组的一个简单方法是,首先初始化固定大小的数组。一旦数组存储满了,创建一个两倍于原始数组大小的新数组。同样,若数组中存储的元素个数小于数组大小的一半,则把数组大小减少一半。
链表的优点
- 常数时间内扩展
- 初始时只需要分配一个元素的存储空间
链表的缺点
- 访问单个元素的时间开销较大(数组是随机存取,即存取数组中任一元素的时间开销为O(1),而链表最差为O(n))
- 存储和检索数据的开销方面却有很大的不足。有时很难对链表操作。如果要删除最后一项,倒数第二项必须更改后继指针值为NULL。这需要从头遍历链表,找到倒数第二个结点的链接,并设置其后继指针为NULL
- 链表中的额外指针引用需要浪费内存
单向链表
链表通常是指单向链表,它包含多个结点,每个结点有一个指向后继元素的next指针。表中最后一个结点的next指针值为null,表示该链表的结束。
链表的类型声明:
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;
}
}
1.链表的基本操作
- 遍历链表
- 在链表中插入一个元素
- 在链表中删除一个元素
2.链表的遍历
假设表头结点的指针指向链表中的第一个结点。遍历链表需完成以下步骤:
- 沿指针遍历
- 遍历时显示结点的内容(或计数)
- 当next指针的值为NULL时结束遍历
统计链表中结点的个数
int ListLength(ListNode headNode){
int length = 0;
ListNode currentNode = headNode;
while(currentNode != null){
length++;
currentNode = currentNode.getNext();
}
return length;
}
3.单向链表的插入
单向链表的插入操作可分为以下3中情况:
- 在链表的表头前插入一个新结点(链表开始处)
- 在链表的表尾后插入一个新结点(链表结尾处)
- 在链表的中间插入一个新结点(随机位置)
3.1在单向链表的开头插入结点
- 更新新节点next指针,使其指向当前的表头结点
- 更新表头指针的值,使其指向新结点
3.2在单向链表的结尾插入结点
- 新结点的next指针指向NULL
- 最后一个结点的next指针指向新结点
3.3在单向链表的中间插入结点
- 如果要在位置3增加一个元素,则需要将指针定位于链表的位置2.即需要从表头开始进过两个结点,然后插入新结点。为了简单起见,假如第二个结点为位置结点,新结点的next指针指向位置结点的下一个结点
- 位置结点的next指针指向新结点
public ListNode InsertInLinkedList(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(headNode);
return nodeToInsert;
}else{//在链表中间或末尾插入
ListNode previousNode = headNode;
int count = 1;
while(count < position-1){
previousNode = previousNode.getNext();
count++;
}
ListNode currentNode = previousNode.getNext();
nodeToInsert.setNext(currentNode);
previousNode.setNext(nodeToInsert);
}
return headNode;
}
单向链表的删除
删除单向链表的第一个结点
- 创建一个临时结点,它指向表头指针所指的结点
- 修改表头指针的值,使其指向下一个结点,并移除临时结点
删除单向链表的最后一个结点
- 遍历链表,在遍历时还要保存前驱结点的地址。当遍历到链表的表尾时,将有两个指针,分别是表尾节点的指针tail及指向表尾结点的前驱结点的指针
- 将表尾的前驱结点的next指针更新为NULL
- 移除表尾结点
删除单向链表中间的一个结点
- 与上一种删除情况类似,在遍历时保存前驱结点的地址。一旦找到要被删除的结点,将前驱结点next指针的值更新为被删除结点的next 指针的值。
- 移除需删除的当前结点。
//单向链表的删除
public 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);
return headNode;
}
if(position == 1){//删除单向链表的表头结点
ListNode currentNode = headNode.getNext();
headNode = null;
return currentNode;
}else{
ListNode previousNode = headNode;
int count = 1;
while(count<position){
previousNode = previousNode.getNext();
count++;
}
ListNode currentNode = previousNode.getNext();
previousNode.setNext(currentNode.getNext());
currentNode = null;
}
return headNode;
}
删除单向链表
将当前结点存储在临时变量中,然后释放当前结点空间的方式来完成。当释放完当前结点后,移动到下一个结点并将其存储在零食变量中,然后不断重复该过程直至释放所有结点
void DeleteLinkedList(ListNode head){
ListNode auxilaryNode,iterator = head;
while(iterator !=null){
auxilaryNode = iterator.getNext();
iterator = null ;//在Java中,垃圾回收器将自动处理
}
}