单向链表
什么是单向链表?
单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。
实现单向链表的构造
单向链表就是如下图一样,类似于铁链一样,元素之间互相连接,有多个节点,每一个节点都有一个指向后边元素的next指针。单向链表的最后一个元素必须指向null。
现在看一下怎么构造单向链表,首先要理解的是JVM是如何创建出链表的,JVM中有堆区和栈区,栈区主要是存储引用,也就是指向实际对象地址(可以理解为链表的表头),而堆区存储的才是创建的对象。
个人理解:链表的数据主要存储在堆区,栈区存的链表表头只是敲门砖,要想访问链表的数据,需要通过栈区的链表表头,访问堆区的链表。且只能使用next遍历。
例:
首先定义这样一个类
public class Course{
Teacher teacher;
Student student;
}
这里的teacher与student就是存在栈中,指向堆的引用。然后如下定义
public class Course{
int val;
Course next;
}
这个时候next就指向了下一个同为Course类型的对象了。如图:
这里就是通过栈中的引用可以找到val(1),然后val(1)节点又存了指向val(2)地址,而val(3)又存了指向val(4)的底子好,所以就构成了一个链条访问结构。
在Java中debug,看单向链表的数据格式是这样的
链表从head开始访问,然后逐个next向后访问,每次所访问对象的类型都是一样的。
单向链表构造的两种方式
1、根据面向对象的理论,Java中规范的链表应该是这样定义:
public class Node {
public int var;
public Node next;
public Node(int var) {
this.var = var;
}
public int getVar() {
return var;
}
public void setVar(int var) {
this.var = var;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
2、但是在LeetCode的算法题中经常使用这样的方式来创建链表:
public class ListNode{
public int val;
public ListNode next;
ListNode(int x){
val = x;
next = null;
}
}
//创建链表
ListNode listNode = new ListNode(1);
这里的val就是当前节点的值,next用来指向下一个节点。因为两个变量都是public的,创建对象后能直接使用listNode.val和listNode.next来操作,虽然违背面向对象的设计要求,但是更精简,所以在算法题目中更广泛。
遍历单向链表的数据
一定要注意的是,对于单向链表,不管执行什么操作,都要从头开始逐个向后访问!!!
遍历链表有多少个节点,代码如下
public static int getListLength(Node head) {
int length = 0;
Node node = head; //要在栈里用一个新的地址指向堆中的数据,防止首节点丢失。
while(node != null){
length++;
//int val = node.next;//表示当前节点数据
node = node.next;//指向下一个节点后且重新给node对象赋值,以此类推可以一个个访问节点内容。
}
return length;
}
插入单向链表的数据
首部插入
注意:head一定要重新指向表头。
现在有一个新节点newNode,然后要连接上现在的表头,那么就用newNode.next = head,这样就是直接连接上表头了,现在newNode就是表头。然后需要把head重新指向表头,那就是head = newNode;
中间插入
尾部插入
首部、中间、尾部插入的代码逻辑实例:
/**
* 链表插入
*
* @param head 链表头节点
* @param nodeInsert 待插入节点
* @param position 待插入位置,从1开始
* @return
*/
public static Node insertNode(Node head, Node newNode, int position) {
if (head == null) {
throw new RuntimeException();
}
int size = getLength(head);//查看链表共有多少个子节点(此方法是上边的遍历节点数据)
if (position > size + 1 || position < 1) { //判断要插入的节点要小于等于链表总长度加一,因为不能隔一个插入 || 判断要插入的节点必须大于等于1
System.out.println("越界");
return head;
}
//首部插入
if (position == 1) {//链表计数从1开始
newNode.next = head;
head = newNode;
return head;
}
//-----------------------------------
//中间和尾部插入
Node pHead = head;//存留首节点,此时head和pHead同时指向堆区中的同一份数据
int count = 1;
while (count < position - 1) {//中间插入时,须找到插入节点的前一位
pHead = pHead.next;
count++;
}
//例:head中有两个节点1、2,position是2,newNode是3
//此时pHead的值是1
newNode.next = pHead.next;//插入到position节点之前(pHead是position的前一位,pHead.next就是position节点)
//上行运行完毕后,newNode的值是3、2
pHead.next = newNode;//此时,pHead.next就相当于是1.next,1的next重新指向一组新的数据也就是newNode
return head;//由于pHead与head指向的是同一份数据,且不是首部插入,所以直接返回head,不用head=pHead
}
删除单向链表的数据
首部、中间、尾部删除的代码逻辑实例:
/**
* 删除节点
*
* @param head 链表头节点
* @param position 删除节点位置,取值从1开始
* @return 删除后的链表头节点
*/
public Node deleteNode(Node head, int position) {
if (head == null) {
throw new RuntimeException();
}
int size = getLength(head);
if (position < 1 || position > size) {
System.out.println("参数错误");
return head;
}
if (position == 1) {
head = head.next;
return head;
} else {
Node pHead = head;
int count = 1;
while (count < position - 1) {
pHead = pHead.next;
count++;
}
Node newNode = pHead.next;//此时newNode节点的数据就是代表着要删除的数据
pHead.next = newNode.next;//执行此语句时,直接把newNode节点也就是要删除的数据给隔过去,也就是删除了
return head;
}
}
部分内容借鉴 编程导航 知识星球 鱼骨头讲师 文档。