链表是一种在物理上非连续、非顺序的数据结构,由若干个节点组成。
链表既然是非连续,非顺序的,它的每一个节点都分布在内存的不同位置,并且依靠着next指针关联起来的,像这样:
分类
链表根据结构又被分成了单向链表和双向链表(但下面的举例全部使用单链表)。
单向链表的每一个节点又包含两部分,一部分是存放数据的变量data,另一部分是指向下一个节点的指针next。
private static class Node{
int data; //存放数据的变量data
Node next; //下一个节点的指针
Node(int data){
this.data = data;
}
}
双向链表每一个节点除了拥有data和next指针,还拥有指向前置节点的prev指针。
private static class Node{
int data; //存放数据的变量data
Node previous; //上一个节点的指针
Node next; //下一个节点的指针
Node(int data){
this.data = data;
}
}
内存中的分配方式
箭头代表next指针:
这样的好处是可以灵活的利用零散空间。
查找节点
链表的查找节点每次只能从头节点开始,然后根据它的next指针一个个节点开始查找,知道找到想要的那个节点。
比如说我现在要找到画框中的第三个节点:
首先,先找到它的头节点;
然后再根据next指针,找到了第二个节点;
第二个节点还不是想要的,同理再去寻找下一个节点;
到三个节点时候,发现是自己想要的节点。
/**
* @param size 链表的实际长度
* @param index 需要查找的位置(0代表链表中第一个位置,取第一个数)
* @return temp 节点指针
* @throws Exception 查找的节点位置超出范围
*/
public Node get(int index)throws Exception{
if(index <0 || index>=size) {
throw new IndexOutOfBoundsException("Out of linked list");
}
//找到head节点,将值赋给temp,用于操作temp去向下查找
Node temp = head;
for(int i=0; i<index; i++) {
//把下一个链表赋值给temp变量,如果index=2,那temp就取到了第三个节点,
//此循环会执行两遍,也就是向后查找了两次(指针向后移动了两次)。
temp = temp.next;
}
return temp;
}
更新结点
更新结点不用考虑很多,找到结点直接替换即可。
现在找到第二个节点,data中是6,现在将它直接覆盖成8就行。
/**
* 修改链表的第index(0-based)个位置的元素为e
* @param index 要更改的位置
* @param newData 更改为的数据
*/
public void set(int index, int newData) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Set failed. Illegal index.");
}
Node cur = head.next;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
//找到位置并替换
cur.data = newData;
}
插入节点
像数组一样,链表插入元素同样有三种情况。
尾部插入
只需要从尾部直接插入,然后把最后一个节点的next指针指向新插入的节点。
头部插入
插入头部节点只需要比尾部插入多一个重新确认头节点的过程。
这就将新节点插入了,并把新插入的节点的next指针指向原来的头节点。
最后再重新确认头节点。
中间插入
中间插入的步骤没有数组那么麻烦,依旧是两个步骤。
首先将新节点的next指针指向插入位置的节点。
再把上一个节点的next指针指向新插入的节点。
只要想插入,根本不用考虑最多可以插入多少的问题。
/**
* 在链表中插入元素
* @param data 要插入的元素
* @param index 要插入的位置
* @param size 链表总长度
* @throws Exception 超出链表节点范围
*/
public void insert(int data,int index) throws Exception{
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Out of linked list");
}
Node insertedNode = new Node(data);
if(size == 0) {
//链表实际长度为0,空链表
head = insertedNode;
last = insertedNode;
}else if(index == 0) {
//插入在链表头部
insertedNode.next = head;
head = insertedNode;
}else if(size == index){
//插入在链表的尾部
last.next = insertedNode;
last = insertedNode;
}else {
//插入在链表的中间
Node previousNode = get(index - 1);//拿到上一个节点
insertedNode.next = previousNode.next;
previousNode.next = insertedNode;
}
size ++;
}
删除节点
尾部删除
要删除掉最后一个节点,然后把最后的节点的next指针指向null。
头部删除
先将头部删除,然后再将头节点重置为next指针指向的下一个节点。
中间删除
中间删除要删除节点后,将前置节点的next指针指向要删除节点的下一个节点。
/**
* 链表删除元素
* @param index 删除的位置
* @param size 链表总长度
* @return removedNode 删除的元素
* @throws Exception 超出链表节点范围
*/
public Node remove(int index)throws Exception{
if(index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Out of linked list");
}
Node removedNode = null;
if(index == 0) {
//删除头节点
removedNode = head;
head = head.next;
}else if(index == size-1) {
//删除尾节点
Node previousNode = get(index-1);
removedNode = previousNode.next;
previousNode.next = null;
last = previousNode;
}else {
//删除中间节点
Node previousNode = get(index-1);
Node nextNode = previousNode.next.next;
removedNode = previousNode.next;
previousNode.next = nextNode;
}
size--;
return removedNode;
}
总结
链表的优势在于能灵活地进行插入和删除操作,所以在插入和删除操作较多的场景下,用链表会更合适。