漫画算法-小灰的算法之旅(03)
1. 什么是链表
链表(linked list):是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。
单向链表:每一个节点又包含两部分,一部分是存放数据的变量data;另一部分是指向下一个节点的指针next.
private static class Node{
int data;
Node next;
}
链表的第一个节点被称为头节点,最后一个节点被称为尾节点,尾节点的next指针指向null。对于链表的其中一个节点A,我们只能根据节点A的next指针来找到该节点的下一个节点B,再根据节点B的next指针找到下一个节点C,依次单线传递。
什么是双向链表?
双向链表:比单向链表稍微复杂一点,它的每一个节点除了拥有data和next指针外,还拥有指向前置节点的prev指针。
链表的存储方式
如果说数组在内存中的存储方式是顺序存储,那么链表在内存中的存储方式则是随机存储。 链表的每一个节点在内存的不同位置,依靠next指针关联起来。这样可以灵活有效的利用零散的碎片空间。
2. 链表的基本操作
查找节点
在查找元素时,链表不像数组那样可以通过下标快速进行定位,只能从头节点开始向后一个一个节点逐一查找。
链表中的数据只能按顺序进行访问,最坏的时间复杂度为O(n)。
更新节点
如果不考虑查找节点的过程,链表的更新过程会像数组那样简单,直接把旧数据换成新数据即可。
插入节点
与数组类似,链表插入节点时,同样分三种情况:
- 尾部插入
- 头部插入
- 中间插入
尾部插入:是最简单的情况,把最后的一个节点的next指针指向新插入的节点即可。
头部插入:第一步:把新节点的next指针指向原先的头节点;第二步:把新节点变为链表的头节点。
中间插入:可以分为两个步骤完成:第一步:新节点的next指针,指向插入位置的节点。第二步:插入位置前置节点的next指针,指向新节点。
只要内存空间允许,能够插入链表的元素时无穷无尽的,不需要像数组那样考虑扩容的问题。
删除节点
链表的删除操作同样分为三种情况。
- 尾部删除
- 头部删除
- 中间删除
尾部删除:是最简单的情况,把倒数第二个节点的next指针指向null即可。
头部删除:也比较简单,把链表的头节点设为原先头节点 的next指针即可。
中间删除:同样很简单,把要删除节点的前置节点的next指针,指向要删除元素的下一个节点即可。
如果不考虑插入、删除操作之前查找元素的过程,只考虑纯粹的插入和删除操作,链表的插入、删除操作时间复杂度都为O(1).
3. 代码实现
实现链表的完整代码
// 头节点指针
private Node head;
// 尾节点指针
private Node last;
// 链表实际长度
private int size;
/**
* 链表插入元素
* param data 插入元素
* param index 插入位置
*/
public void insert(int data,int index) throws Exception{
if(index<0 || index>size){
throw new IndexOutOfBoundsException("超出链表节点范围");
}
Node insertNode =new Node(data);
if(size == 0){
// 空链表
head =insertNode;
last =insertNode;
}else if(index ==0){
//插入头部
insertNode.next=head;
head =insertNode;
}else if (size ==index){
//尾部插入
last.next=insertNode;
last =insertNode;
}else{
//插入中间
Node prevNode=get(index-1);
insertNode.next=prevNode.next;
prevNode.next=insertNode;
}
size++;
}
/**
* 链表删除元素
* param index 删除的位置
*
*/
public Node remove(int index ) throws Exception{
if(index<0 || index >size){
throw new IndexOutOfBoundsException("超出链表节点范围");
}
Node removedNode =null;
if(index ==0){
//删除头节点
removeNode =head;
head=head.next;
}else if (index ==size-1){
//删除尾节点
Node prevNode =get(index-1);
removedNode =prevNode.next.next;
prevNode.next =null;
last =prevNode;
}else{
// 删除中间节点
Node prevNode= get(index-1);
Node nextNode= prevNode.next.next;
removedNode=prevNode.next;
prevNode.next=nextNode;
}
size--;
return removedNode;
}
/**
* 链表查找元素
* param index 查找的位置
*/
public Node get(int index) throws Exception{
if(index<0 || index>=size){
throw new IndexOutOfBoundsException("超出链表节点范围");
}
Node temp =head;
for(int i=0;i<index;i++){
temp=temp.next;
}
return temp;
}
/**
* 输出链表
*/
public void output(){
Node temp=head;
while(temp!=null){
System.out.println(temp.data);
temp=temp.next;
}
}
/**
* 链表节点
*/
private static class Node{
int data;
Node next;
Node(int data){
this.data=data;
}
}
public static void main(Strings[] args) throws Exception{
MyLinkedList myLinkedList=new MyLinkedList();
myLinkedList.insert(3,0);
myLinkedList.insert(7,1);
myLinkedList.insert(9,2);
myLinkedList.insert(5,3);
myLinkedList.insert(8,1);
myLinkedList.insert(6,7);
myLinkedList.remove(0);
myLinkedList.output();
}
以上是对单链表相关操作的代码实现。为了尾部插入的方便,代码中额外增加了指向链表尾部节点的指针last。
4. 数组与链表的比较
数据结构没有绝对的好与坏,数组和链表各有千秋。数组和链表相关操作的性能比较如下表所示:
查找 | 更新 | 插入 | 删除 | |
---|---|---|---|---|
数组 | O(1) | O(1) | O(n) | O(n) |
链表 | O(n) | O(1) | O(1) | O(1) |
从表格可以看出,数组的优势在于能够快速定位元素,对于读操作多,写操作少的场景来说,用数组更合适一些。
链表的优势在于能够灵活的进行插入和删除操作,如果需要在尾部频繁插入、删除元素,用链表更合适一些。