上期内容我们讲述了顺序表,知道了顺序表的底层是一段连续的空间进行存储(数组),在插入元素或者删除元素需要将顺序表中的元素整体移动,时间复杂度是O(n),效率比较低。因此,在Java的集合结构中又引入了链表来解决这一问题。
1、链表
如上图所示是单向链表节点的两个元素:其中value存储着节点的值,next存储着下一个节点的地址。因此,一个单向链表可以表示为下图所示:
注意 :
1、从上图可以看出,链式结构在逻辑上是连续的,但在物理上(即在计算机的内存里面)不一定连续。
2、 现实中的节点一般是从堆上申请出来的。
3、从堆上申请的空间,是按照一定策略来分配的,两次申请的空间可能连续,也可能不连续。
2、链表结构
链表组合起来的结构一共有8种,通过以下情况进行排列组合:
2.1 单向或者双向
单向
双向
双向链表在next域的基础上增加了prev域,使得通过链表的一个节点不仅能访问后继元素,也能访问前驱元素 。
2.2 带头或者不带头
带头
注意:这里的头并没有实际的值,主要用它链接后续的节点,因此,head指向第一个元素的地址。
不带头
2.3 循环或者非循环
循环
非循环
这里我们重点讲单向不带头非循环链表和双向不带头非循环链表。
3、无头单向非循环链表实现
3.1 节点的实现
链表的节点主要通过静态内部类进行实现。代码如下:
static class ListNode{
public int val;//节点的值域
public ListNode next;//下一个节点的地址
public ListNode() {
}
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;//表示当前链表的头结点
这里可能有人会有疑问:为什么不把头结点的声明放入内部类中呢?
其实,从逻辑上想,不难想明白:头结点是属于整个链表的头结点,而非结点的头结点。
3.2 creatNode方法
本方法用于初始化一个链表。
public void creatNode(){
ListNode node1 = new ListNode(12);
ListNode node2 = new ListNode(23);
ListNode node3 = new ListNode(34);
ListNode node4 = new ListNode(45);
ListNode node5 = new ListNode(56);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
this.head = node1;
}
3.3 头插法
故名思义:就是在链表的头部插入。这里有个点需要注意:先插入的数据会最后输出,后插入的最先输出。
在头部插入一个元素,我们需要先绑定元素后面的信息,再让我们的头结点指向我们要插入的元素。代码如下:
//头插法
public void addFirst(int data){
ListNode node = new ListNode(data);
//一般建议在插入的时候先绑定后面的节点信息
node.next = head;
this.head = node;
}
3.4 尾插法
在尾部插入一个元素,需要先绑定前面的元素的信息,即让最后一个元素的next指向要插入的元素。
注意:如果链表中没有元素,则直接让头结点指向要插入的元素。
//尾插法
public void addLast(int data){
ListNode cur = head;
ListNode node = new ListNode(data);
if(cur == null){
//如果链表中没有元素,那么让头结点指向node
head = node;
return;
}
//当cur.next == null的时候,那么该节点就是尾巴节点
//当cur == null证明该链表已经被遍历完了
while (cur.next != null){
cur = cur.next;
}
cur.next = node;
}
3.5 任意位置插入,第一个数据节点为0号下标
在任意位置插入,首先,我们要判定下标是否合法,若不合法则抛出异常。第二步,如果插入的下标为0,则直接调用前面写的头插法函数,如果下标等于链表的长度,则调用尾插法函数。第三步,如果插入的地方是在中间,则需要先找到要插入结点的前一个结点,绑定后面的信息后再进行插入。
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
if(index < 0 || index > size()){
throw new IndexErrorException("下标不合法");
}
if(index == 0){
addFirst(data);
return;
}
else if (index == size()){
addLast(data);
return;
}
//1、定义cur走index-1步
//2、进行插入
ListNode node = new ListNode(data);
ListNode cur = head;
int count = 0;
while(count != index-1){
cur = cur.next;
count++;
}
node.next = cur.next;
cur.next = node;
}
3.6 查找关键字key是否在单链表当中
查找关键字key只需要遍历链表,看结点的value是否等于key就ok了。
//查找是否包含关键字key是否在单链表当中
public boolean contains(int key){
ListNode cur = head;
while (cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
3.7 删除第一次出现关键字key的元素
思路:删除一个结点(del),我们需要记录被删除结点的前一个结点(cur),再让前一个结点的next域(cur.next)指向被删除结点的下一个结点(del.next)。需要注意的是:在这个过程之前需要单独删除头结点。
//删除第一次出现关键字为key的节点
public void remove(int key){
if(head == null){
return;
}
//单独删除头结点
if(head.val == key){
head = head.next;
return;
}
//找到删除节点的前一个结点
ListNode cur = head;
while (cur.next != null)
{
ListNode del = cur.next;
if(del.val == key){//此时cur指向的节点就是要删除节点的前一个节点
//cur.next = cur.next.next
cur.next = del.next;
return;//删除之后返回
}
cur = cur.next;
}
if(cur==null){
System.out.println("没有你要删除的数字");
}
}
3.8 删除所有值为key的节点
删除所有值为key的结点需要用到双指针的思想(ps:因为需要不断地记录被删除结点的前一个结点), prev指针用来找到被删除元素的前一个元素,cur指针则用来遍历链表和找到被删除元素。删除的过程就是直接让prev的next域直接指向cur指针的next域,再让cur指针向后走;若不是被删除的元素则直接让两个指针一起往后走。最后,我们需要单独删除头结点。(注意:这里我们在最后才删除头结点是因为前面的cur和prev指针需要使用到头结点)。
//删除所有值为key的节点
public void removeAllKey(int key){
if(head == null){
return;
}
ListNode prev = head;
ListNode cur = head.next;
while (cur != null){
if(cur.val == key){
prev.next = cur.next;
cur = cur.next;
}else{
prev = cur;
cur = cur.next;
}
}
//单独删除头结点
if(head.val == key){
head = head.next;
}
}
3.9 得到单链表长度
得到单链表的长度,我们需要定义一个计数器,然后遍历链表让count++就行了。
//得到单链表的长度
public int size(){
int count = 0;
ListNode cur = head;
while(cur != null){
count++;
cur = cur.next;
}
return count;
}
3.10 清空链表
遍历链表将链表的每个结点置空即可。
//清空链表
public void clear() {
ListNode cur = head;
while (cur != null){
cur = null;
cur = cur.next;
}
}
3.11 打印链表
遍历链表,打印每个结点的value域即可。
//打印链表
public void display() {
ListNode cur = head;
while (cur!= null){//如果cur==null证明把链表遍历完成了
System.out.print(cur.val + " ");
cur = cur.next;//cur每次都在向后走一步
}
System.out.println();
}