1.1链表的概念及结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
单向、双向
带头、不带头
循环、非循环
-
不带头结点的非循环单向链表
-
不带头结点的非循环双向链表
-
不带头结点的循环单向链表
-
不带头结点的循环双向链表
-
带头结点的非循环单向链表(头结点不存放数据)
-
带头结点的非循环双向链表(头结点不存放数据)
-
带头结点的循环单向链表(头结点不存放数据)
-
带头结点的循环双向链表(头结点不存放数据)
虽然有这么多的链表的结构,但是现在先重点实现无头单向非循环链表和无头双向非循环链表,其余的后面在实现。
1.2链表的简单实现
1.2.1无头单向非循环链表的实现
首先先创建一个节点类,里面包含了data数据域和next域
class Node{
public int data;
public Node next;
public Node(int data) {
this.data = data;
}
}
然后就是链表的接口和方法实现
public class MyLinkNode {
//定义一个链表的头待连接链表
public Node head;
}
注意:以下方法均在MyLinkNode 类中
(1)显示单链表
public void disPlay() {
if(this.head==null) {
System.out.println("Over!");
return;
}
Node p = this.head;
while(p!=null) {
System.out.print(p.data+"->");
p = p.next;
}
System.out.println("Over!");
}
(2)头插法
头插法不需要判空
图示:
代码:
public void addFirst(int data) {
Node node = new Node(data); //创建一个新的节点
Node node = new Node(data);
node.next = this.head;
this.head = node;
}
(2)尾插法
尾插法需要判空
图示:
代码:
public void addEnd(int data) {
Node node = new Node(data);
//如果头为空,则直接让头指向新节点
if(this.head == null) {
this.head = node;
}else {
Node p = this.head;
//先找到链表的最后一个结点
while(p.next!=null) {
//如果当前结点的next不为空,则说明后面还有结点
//如果为空,则说明已找到最后一个结点的位置
p = p.next;
}
//让最后一个结点的next等于新结点
p.next = node;
}
}
(3)获取单链表的长度
public int size() {
int count = 0;
if(this.head == null) {
//如果头为空,返回0
return 0;
}else {
Node p = this.head;
//采用遍历单链表的思路
//每走过一个结点,count++,直到当前结点为空
while(p!=null) {
count++;
p = p.next;
}
}
return count;
}
(4)任意位置插入,第一个数据节点为0号下标
要在任意位置插入,首先要判断下标合法性
public boolean checkIndex(int index) {
if(index < 0||index > this.size()) {
return false;
}
return true;
}
然后,查找 (index-1)节点的位置,也就是[前驱节点]的位置,找到返回该节点
public Node searchPrev(int index) {
Node prev = this.head;
int i = 0;
while(i<index-1) {
prev = prev.next;
i++;
}
return prev;
}
最后,根据下标插入节点(当然,你也可以将这些都写到一个方法中)
图示:
代码:
public void addIndex(int index,int data) {
Node node = new Node(data);
Node prev = null;
//如果下标位置非法,直接return
if(!this.checkIndex(index))
{
return;
}
if(index==0) {
//如果下标位置为0,则直接调用头插法
addFirst(data);
return;
}
if(index==this.size()) {
//如果下标位置等于链表的长度,调用尾插法
addEnd(data);
return;
}else {
//接收前驱节点
prev = this.searchPrev(index);
node.next = prev.next;
prev.next = node;
}
}
(5)查找是否包含关键字key是否在单链表当中
public boolean findVal(int data) {
//思路简单,采用遍历的方法
Node p = this.head;
while(p!=null) {
if(p.data==data) {
return true;
}
p = p.next;
}
return false;
}
(6)删除第一次出现关键字为key的节点
首先找到要删除的结点的前驱节点
public Node searchPrevNode(int key) {
Node prev = this.head;
//同样采用遍历的思路
if(findVal(key)) {
while(prev.next!=null) {
if(prev.next.data==key) {
return prev;
}
prev = prev.next;
}
}
return null;
}
然后分情况删除节点
图示:
代码:
public void remove(int key) {
//接收待删除结点的前驱节点
Node prev = this.searchPrevNode(key);
Node p = null;
//如果链表为空,则直接返回
if(this.head==null) {
return;
}
//特殊情况 头删
if(this.head.data==key) {
this.head = this.head.next;
System.out.println("删除成功!");
return;
}
//如果不存在前驱节点,说明要删除的节点不存在
if(prev==null) {
System.out.println("不存在该值!");
return;
}else {
p = prev.next;
prev.next = p.next;
System.out.println("删除成功!");
}
}
(7)删除所有值为key的节点
图示:(假设删除所有值为1的节点)
代码:
public void removeAllKey(int key) {
if(head==null){
return;
}
Node p = this.head.next;
Node prev = this.head;
//先调用查找方法确定待删除的结点是否存在
if(!findVal(key)) {
System.out.println("不存在该值!");
return;
}
//也可以对头节点的判断放到下面的while循环里
// while(this.head.data==key&&this.head.next!=null) {
// this.head = this.head.next;
// p = this.head.next;
// prev = this.head;
// }
// if(p==null&&this.head.data==key) {
// this.head=null;
// }
while(p!=null) {
if(p.data==key) {
prev.next = p.next;
p = p.next;
}else {
prev = p;
p = p.next;
}
if(this.head.data==key) {
this.head = this.head.next;
}
}
System.out.println("删除成功!");
}
(8)尾删法
public void popBack() {
Node p = this.head;
Node prev = null;
if(this.head==null) {
//如果链表头为空,则直接返回
return;
}
if(this.head.next==null) {
//如果表头的next为空,则说明要删除的为链表头
this.head = null;
System.out.println("尾删成功!");
return;
}
//通过遍历找到最后一个节点的前驱节点位置
while(p.next!=null) {
prev = p;
p= p.next;
}
prev.next = null;
System.out.println("尾删成功!");
}
(9)清空单链表
public void clear() {
if(this.head==null) {
return;
}
this.head = null;
}
1.2.2无头双向非循环链表的实现
首先先创建一个节点类,里面包含了data数据域、next域、prev域
class Node{
public int data;
public Node next;
public Node prev;
public Node(int data) {
this.data = data;
}
}
然后就是链表的接口和方法实现
public class MyDoubleLinkNode {
//定义双向链表的头和尾节点用来标记链表的头部和尾部
public Node head;//头节点
public Node tail;//尾节点
}
注意:以下方法均在MyDoubleLinkNode类中
(1)显示单链表
public void display() {
if(this.head==null) {
System.out.println("Over!");
return;
}
Node p = this.head;
while(p!=null) {
//依旧采用循环遍历的方式
System.out.print(p.data+"->");
p = p.next;
}
System.out.println("Over!");
}
(2)头插法
图示:
代码:
public void addFirst(int data) {
Node node = new Node(data);
if(this.head==null) {
//如果表头为空,则让head和tail都指向新节点node
this.head = node;
this.tail = node;
}else {
node.next = this.head;
this.head.prev = node;
this.head = node;
}
}
(3)尾插法
图示:
代码:
public void addLast(int data) {
Node node = new Node(data);
if(this.head==null) {
//如果表头为空,则让head和tail都指向新节点node
this.head = node;
this.tail = node;
}
//由于有表尾指针,所以不用遍历寻找最后一个节点的位置
this.tail.next = node;
node.prev = this.tail;
this.tail = node;
}
(4)单链表长度
public int size() {
int count = 0;
Node p = this.head;
while(p!=null) {
//依然采用遍历思路
count++;
p = p.next;
}
return count;
}
(5)任意位置插入,第一个数据节点为0号下标
图示:
代码:
public boolean addIndex(int index,int data) {
if(index < 0) {
return false;
}
int size = this.size();
if(index == 0) {
//index==0调用头插法
this.addFirst(data);
return true;
}
if(index == size) {
//index==size调用尾插法
this.addLast(data);
return true;
}
Node p = this.head;
Node node = new Node(data);
int n = index - 1;
while(n>0) {
//通过遍历index-1次找到插入位置的前驱节点
p = p.next;
n--;
}
p.next.prev = node;
node.next = p.next;
node.prev = p;
p.next = node;
return true;
}
(6)查找是否包含关键字key是否在单链表当中
public boolean contains(int key) {
if(this.head==null) {
return false;
}
Node p = this.head;
while(p!=null) {
//依然采用遍历思路
if(p.data == key) {
return true;
}
p = p.next;
}
return false;
}
(7)删除第一次出现关键字为key的节点
图示:
代码:
public void remove(int key) {
//如果表中没有该关键字直接返回
if(!this.contains(key)) {
return;
}
Node p = this.head;
while(p!=null) {
//当关键字相等时,需要分情况
if(p.data==key) {
if(p == this.head) {
//当删除表头节点时
this.head = this.head.next;
this.head.prev = null;
}else {
p.prev.next = p.next;
if(p.next!=null) {
//删除的不是尾节点
p.next.prev = p.prev;
}else {
//说明删除的是尾节点
this.tail = p.prev;
}
}
//到这删除完毕
return;
}else {
//只要关键值相等p就不往下走了,换言之不相等,p才会往下走
p = p.next;
}
}
}
(8)删除所有值为key的节点
public void removeAllKey(int key) {
if(this.head==null) {
return;
}
Node p = this.head;
while(p!=null) {
if(p.data==key) {
if(p == this.head) {
this.head = this.head.next;
this.head.prev = null;
}else {
p.prev.next = p.next;
if(p.next!=null) {
//删除的不是尾节点
p.next.prev = p.prev;
}else {
this.tail = p.prev;
}
}
}
//跟删除第一次出现关键字为key的节点不同的是让p一直往下走,直到p==null,就可以删除所有的值为关键字的节点了
p = p.next;
}
}
(9)清空单链表
public void clear() {
if(this.head == null) {
return;
}
Node p = this.head;
while(p!=null) {
//通过遍历让每个节点的next和prev都指向null
Node s = p.next;
p.next = null;
p.prev = null;
p = s;
}
this.head = null;
this.tail = null;
}
1.3总结
- 链表以节点为单位存储,不支持随机访问
- 任意位置插入时间复杂度为O(1)。
- 无扩容问题,插入一个开辟一个空间。
- 对于链表一些方法,建议多画图。