单链表
线性表的顺序存储结构。它是有缺点的,最大的缺点就是插入和删除时需要移动大量元素,这显然就需要耗费时间。能不能想办法解决呢?
要解决这个问题,我们就得考虑一下导致这个问题的原因。
为什么当插入和删除时,就要移动大量元素,仔细分析后,发现原因就在于相邻两元素的存储位置也具有邻居关系。它们编号是1,2,3,…,n,它们在内存中的位置也是挨着的,中间没有空隙,当然就无法快速介入,而删除后,当中就会留出空隙,自然需要弥补。问题就出在这里。
我们反正也是要让相邻元素间留有足够余地,那干脆所有的元素都不要考虑相邻位置了,哪有空位就到哪里,而只是让每个元素知道它下一个元素的位置在哪里,这样,我们可以在第一个元素时,就知道第二个元素的位置(内存地址),而找到它;在第二个元素时,再找到第三个元素的位置(内存地址)。这样所有的元素我们就都可以通过遍历而找到。
以前在顺序结构中,每个数据元素只需要存数据元素信息就可以了。现在链式结构中,除了要存数据元素信息外,还要存储它的后继元素的存储地址。
为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。
我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息,称为结点(Node)。
n个结点链结成一个链表,即为线性表(a1,a2,…,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起
我们的链表包含4项数据:“a”、“b”、“c"和"d”。因为每个结点都需要2个格子,头一格用作数据存储,后一格用作指向下一结点的链(最后一个结点的链是null,因为它是终点),所以整体占用了8个格子。
若想使用链表,你只需知道第一个结点在内存的什么位置。因为每个结点都有指向下一结点的链,所以只要有给定的第一个结点,就可以用结点1的链找到结点2,再用结点2的链找到结点3……如此遍历链表的剩余部分。
链表相对于数组的一个好处就是,它可以将数据分散到内存各处,无须事先寻找连续的空格子。
对于线性表来说,总得有个头有个尾,链表也不例外。我们把链表中第一个结点的存储位置叫做头指针,那么整个链表的存取就必须是从头指针开始进行了。之后的每一个结点,其实就是上一个的后继指针指向的位置。
类似像一个铁链,一环扣一环地连在一起
想象一下,最后一个结点,它的指针指向哪里?最后一个,当然就意味着直接后继不存在了,所以我们规定,线性链表的最后一个结点指针为“空”(通常用NULL或“^”符号表示)。
有时,我们为了更加方便地对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点。头结点的数据域可以不存储任何信息。也可以存储如线性表的长度等附加信息, 头结点的指针域存储指向第一个结点的指针
带有头节点的链表就像是给铁链加了钥匙扣
Node{
int data;
Node next;
}
单链表的基本操作
下面讲解单链表的初始化、创建、取值、查找、插入、删除等基本操作。
初始化
单链表的初始化是指构建一个空表。先创建一个头节点,不存储数据,然后令其指针域为空
创建
创建单链表分为头插法和尾插法两种,头插法是指每次把新节点插到头节点之后,其创建的单链表和数据输入顺序正好相反,因此也称为逆序建表。尾插法是指每次把新节点链接到链表的尾部,其创建的单链表和数据输入顺序一致,因此也称为正序建表。
头插
我们先讲头插法建表。头插法每次把新节点插入到头节点之后,创建的单链表和数据输入顺序相反。
1)初始状态。初始状态是指初始化后的空表,只有一个头节点
2)输入数据元素1,创建新节点,把元素1放入新节点数据域
3)头插操作,插入头节点的后面
4)输入数据元素2,创建新节点,把元素2放入新节点数据域
5)头插操作,插入头节点的后面
赋值解释
假设赋值之前节点的地址及指针
赋值语句两端,等号的右侧是节点的地址,等号的左侧是节点的指针域。
① s->next=L->next : L->next存储的是下一个节点地址“9630”,将该地址赋值给s->next指针域,即s节点的next指针指向1节点
② L->next=s:将s节点的地址“2046”赋值给L->next指针域,即L节点的next指针指向s节点
注意修改指针顺序
要先修改后面那个指针
因为一旦修改了L节点的指针域指向s,那么原来L节点后面的节点就找不到了,因此修改指针是有顺序的。修改指针的顺序原则:先修改没有指针标记的那一端
如果要插入节点的两端都有标记,例如,再定义一个指针q指向L节点后面的节点,那么先修改哪个指针都无所谓了
6)拉直链表之后,如图
7)继续依次输入数据元素3、4、5、6、7、8、9、10,头插法创建的单链表如图所示。可以看出,头插法创建的单链表与数据输入顺序正好相反。
尾插法
尾插法每次把新节点链接到链表的尾部,其创建的单链表和数据输入顺序一致。
尾插法每次把新节点链接到链表的尾部,因此需要一个尾指针永远指向链表的尾节点。
1)初始状态。初始状态是指初始化后的空表,只有一个头节点,设置一个尾指针r指向该节点,如图
2)输入数据元素1,创建新节点,把元素1放入新节点数据域,如图
3)完成尾插操作,插入尾节点的后面,如图
赋值解释
① s->next=NULL:s节点的指针域置空。
② r->next=s:将s节点的地址赋值给r节点的指针域,即将新节点s插入尾节点r之后。
③ r=s:将s节点的地址赋值给r,即r指向新的尾节点s。
4)输入数据元素2,创建新节点,把元素2放入新节点数据域,如图
5)完成尾插操作,插入尾节点的后面,如图
6)继续依次输入数据元素3、4、5、6、7、8、9、10,尾插法创建的单链表如图所示。可以看出,尾插法创建的单链表与数据输入顺序一致。
创建遍历节点
package com.maweiqi.linked;
/**
* 链表节点
*/
public class Node {
//数据域
public Integer data;
//指针域,指向下一个节点
public Node next;
public Node() {
}
public Node(Integer data, Node next) {
this.data = data;
this.next = next;
}
public Node(Integer data) {
this.data = data;
}
}
/**
*
* SingleLinkedList 定义的链表
*/
public class SingleLinkedList {
//定义一个head头结点
private static Node head = new Node();
//向链表添加数据
public static void addData(int value){
//初始化要添加的节点
Node newNode = new Node(value);
//设置临时节点
Node temp = head;
//找到尾节点
while (temp.next != null){
temp = temp.next;
}
//已经包括了头结点.next为null的情况了
temp.next = newNode;
}
//遍历链表
public static void traverse(Node head){
//定义临时节点,从首节点开始
Node temp = head.next;
while (temp != null){
if(temp.data != null){
System.out.println("当前节点的data为: "+temp.data);
}
//继续找下一个节点
temp = temp.next;
}
}
public static void main(String[] args) {
addData(11);
addData(22);
addData(33);
addData(44);
traverse(head);
}
}
输出
当前节点的data为: 11
当前节点的data为: 22
当前节点的data为: 33
当前节点的data为: 44
指定任意位置插入
单链表的插入是指在单链表的第pos-1个结点和第pos个结点之间插入一个新的结点,要实现结点的插入,需修改插入位置之前的结点和当前插入结点的指针指向,过程如图
插入操作,首先检查参数pos的合法性,插入操作的第二步是s.next=q,对应要算法中的语句是Node s= new Node;第三步是p所引用结点的指针域指向s所引用结点,语句是p.next=s;插入操作完成后链表长度加1
package com.maweiqi.linked;
/**
*
* SingleLinkedList 定义的链表
*/
public class SingleLinkedList {
//定义一个head头结点
private static Node head = new Node();
//向链表添加数据
public static void addData(int value){
//初始化要添加的节点
Node newNode = new Node(value);
//设置临时节点
Node temp = head;
//找到尾节点
while (temp.next != null){
temp = temp.next;
}
//已经包括了头结点.next为null的情况了
temp.next = newNode;
}
//遍历链表
public static void traverse(Node head){
//定义临时节点,从首节点开始
Node temp = head.next;
while (temp != null){
if(temp.data != null){
System.out.println("当前节点的data为: "+temp.data);
}
//继续找下一个节点
temp = temp.next;
}
}
/**
* 插入节点
* @param head 头指针
* @param index 要插入元素的位置
* @param value 要插入元素的值
*/
public static void insertNode(Node head, int index, int value){
//首先判断指定的位置是否合法
if(index < 1 || index > linkListLength(head) + 1){
System.out.println("插入位置不合法");
return;
}
//设置临时节点,从头结点开始
Node temp = head;
//记录遍历当前的位置
int currentPos = 0;
//初始化插入的节点
Node insertNode = new Node(value);
//循环进行操作
while(temp.next != null){
//找到上一个节点的位置
if((index -1) == currentPos){
//temp 就是代表上一个元素,将原本上一个节点的指向交给由插入元素节点来指向
insertNode.next = temp.next;
//将上一个节点的指针指向要插入的节点
temp.next = insertNode;
return;
}
currentPos++;
temp = temp.next;
}
}
/**
* 获取链表长度的方法
* @param head
* @return
*/
private static int linkListLength(Node head) {
int length = 0;
//设置临时节点,从首节点开始
Node temp = head.next;
//找尾节点,累加length
while (temp != null){
length++;
temp = temp.next;
}
return length;
}
public static void main(String[] args) {
addData(11);
addData(22);
addData(33);
addData(44);
insertNode(head,3,88);
traverse(head);
}
}
输出
当前节点的data为: 11
当前节点的data为: 22
当前节点的data为: 88
当前节点的data为: 33
当前节点的data为: 44
删除
删除一个节点,实际上是把这个节点跳过去。根据单向链表向后操作的特性,要想跳过第i个节点,就必须先找到第i-1个节点,否则是无法跳过去的。删除操作如图
赋值解释
p->next=q->next的含义是将q节点的下一个节点地址赋值给p节点的指针域。
对这些有关指针的赋值语句,很多读者不理解,容易混淆。在此说明一下,等号的右侧是节点的地址,等号的左侧是节点的指针域,如图
假设q节点的下一个节点地址为1013,该地址存储在q->next里面,因此等号右侧的q->next的值为1013。把该地址赋值给p节点的next指针域,把原来的值2046覆盖掉,这样p->next也为1013,相当于把q节点跳过去了。赋值之后,如图2-58所示,然后用delete q释放被删除节点的空间。
在单链表中,每个节点除存储自身数据之外,还存储了下一个节点的地址,因此可以轻松访问下一个节点,以及后面的所有后继节点。但是,如果想访问前面的节点就不行了,再也回不去了。例如,删除节点q时,要先找到它的前一个节点p,然后才能删掉q节点,单向链表只能向后操作,不可以向前操作。如果需要向前操作,该怎么办呢?
还有另外一种链表——双向链表。
package com.maweiqi.linked;
/**
*
* SingleLinkedList 定义的链表
*/
public class SingleLinkedList {
//定义一个head头结点
private static Node head = new Node();
//向链表添加数据
public static void addData(int value){
//初始化要添加的节点
Node newNode = new Node(value);
//设置临时节点
Node temp = head;
//找到尾节点
while (temp.next != null){
temp = temp.next;
}
//已经包括了头结点.next为null的情况了
temp.next = newNode;
}
//遍历链表
public static void traverse(Node head){
//定义临时节点,从首节点开始
Node temp = head.next;
while (temp != null){
if(temp.data != null){
System.out.println("当前节点的data为: "+temp.data);
}
//继续找下一个节点
temp = temp.next;
}
}
/**
* 插入节点
* @param head 头指针
* @param index 要插入元素的位置
* @param value 要插入元素的值
*/
public static void insertNode(Node head, int index, int value){
//首先判断指定的位置是否合法
if(index < 1 || index > linkListLength(head) + 1){
System.out.println("插入位置不合法");
return;
}
//设置临时节点,从头结点开始
Node temp = head;
//记录遍历当前的位置
int currentPos = 0;
//初始化插入的节点
Node insertNode = new Node(value);
//循环进行操作
while(temp.next != null){
//找到上一个节点的位置
if((index -1) == currentPos){
//temp 就是代表上一个元素,将原本上一个节点的指向交给由插入元素节点来指向
insertNode.next = temp.next;
//将上一个节点的指针指向要插入的节点
temp.next = insertNode;
return;
}
currentPos++;
temp = temp.next;
}
}
/**
* 获取链表长度的方法
* @param head
* @return
*/
private static int linkListLength(Node head) {
int length = 0;
//设置临时节点,从首节点开始
Node temp = head.next;
//找尾节点,累加length
while (temp != null){
length++;
temp = temp.next;
}
return length;
}
/**
* 删除节点
*/
public static void deleteNode(Node head ,int index){
//首先判断指定的位置是否合法
if(index < 1 || index > linkListLength(head) + 1){
System.out.println("插入位置不合法");
return;
}
//设置临时节点
Node temp = head;
//记录遍历当前的位置
int currentPos = 0 ;
while (temp.next != null){
//先找到上一个节点的位置
if((index - 1) == currentPos){
//temp表示上一个节点,我们想删除的是temp.next节点
//将想要删除的节点进行存储
Node deleteNode = temp.next;
//想要删除: 节点的下一个节点交友上一个节点来指向
temp.next = deleteNode.next;
deleteNode = null;
return;
}
currentPos++;
temp = temp.next;
}
}
public static void main(String[] args) {
addData(11);
addData(22);
addData(33);
addData(44);
deleteNode(head,2);
traverse(head);
}
}
输出
当前节点的data为: 11
当前节点的data为: 33
当前节点的data为: 44