文章目录
链表概念及结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。相较于顺序表,链表更适合数据的插入与删除。
链表的结构一共有八种,分别为:单向与双向,带头与不带头,循环与非循环,无头单向非循环链表,无头双向链表(LinkedList底层实现就是无头双向循环链表)。
1. 无头单向非循环链表
虽然无头单向非循环链表的结构很简单,通常不会单独用来存储数据,但在笔试面试中常出现,接下来将对无头单向非循环链表以及里面的方法进行实现。
定义链表的节点和对象
首先定义MyLinkedList用于实现对链表进行操作的方法,再在其中定义一个内部类ListNode —— 用于存放节点数据和指向下一个节点的引用变量,然后在MyLinkedList类中定义一个ListNode类的成员变量head —— 用于标记链表的头
public class MyLinkedList {
static class ListNode{
public int val;
public ListNode next;
public ListNode (int val) {
this.val = val;
}
}
private ListNode head;
}
实现add方法:插入元素
头插法
先用data值创建一个新节点,让新节点指向head,再把head设置为新节点
public void addFirst(int data) {
ListNode list = new ListNode(data);
if(head == null) {
head = list;
return;
}
list.next = head;
head = list;
}
尾插法
先判断当前链表是否为空,若为空,直接让head指向新节点即可,若不为空,因为head是一直指向链表头节点的,不好改变,所以则需定义一个ListNode的引用变量cur,让其从头遍历到链表最后一个节点,再令最后一个节点的next指向新节点
public void addLast(int data) {
ListNode list = new ListNode(data);
ListNode cur = this.head;
if(this.head == null){
this.head = list;
}else{
while (cur.next != null){
cur = cur.next;
}
cur.next = list;
}
}
指定位置插入元素
首先要判断指定插入的位置是否合法,保证插入的位置在链表长度范围内,这时先写个size方法获取链表长度,进行判断位置合法性,若不合法则报出异常,此处自定义了一个异常。
若index = 1,则和头插法进行的操作一样,直接调用头插法就好。
同理,index为链表长度时,直接调用尾插法就好。
//第一个数据节点为0号下标
public void addIndex(int index,int data) throws OutOfIndexException {
if(index < 0 || index > this.size()) {
throw new OutOfIndexException("下标错误");
}
if(index == 1) {
addFirst(data);
return;
}
if(index == this.size()) {
addLast(data);
return;
}
ListNode list = new ListNode(data);
ListNode cur = head;
for (int i = 1; i < index; i++) {
cur = cur.next;
}
list.next = cur.next;
cur.next = list;
}
//得到单链表的长度
public int size() {
if(head == null) {
return 0;
}
int count = 0;
ListNode cur = head;
while (cur != null) {
cur = cur.next;
count++;
}
return count;
}
打印链表
遍历链表一遍打印
//打印链表
public void display() {
if(head == null) {
return;
}
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println( );
}
查找包含关键字key的节点是否在单链表当中
如果链表为空,则返回false,如果不为空,则遍历链表一遍查找key的值。
public boolean contains(int key) {
if(head == null) {
return false;
}
ListNode cur = head;
while(cur != null) {
if(cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
删除第一次出现关键字为key的节点
首先进行两个判断:
1.链表为空,若为空进入不了循环,什么也不干
2.值为key的节点是否是head,如果是head直接让head指向下一个节点即可;如果不是head,需要两个引用变量,一个指向值为key的节点(cur),一个指向cur的前一个节点(pre),遍历链表,如果找到key,则直接让pre指向cur的下一个节点即可。
public void remove(int key) {
ListNode cur = head;
ListNode pre = cur;
while(cur != null) {
if(cur.val == key) {
if(cur != head) {
pre.next = cur.next;
}else {
head = head.next;
}
return;
}else {
pre = cur;
cur = cur.next;
}
}
}
删除所有值为key的节点
和前者删除节点相似,只是删除一个节点之后不return,继续遍历链表,删除值为key的节点
public void removeAllKey(int key) {
ListNode cur = head;
ListNode pre = cur;
while(cur != null) {
if(cur.val == key) {
if(cur != head) {
pre.next = cur.next;
}else {
head = head.next;
}
}
pre = cur;
cur = cur.next;
}
}
清空链表
直接将head置为null即可。
public void clear() {
this.head = null;
}
2. 无头双向链表节点
和单链表相似,不过是双链表内部类ListNode中多了一个变量,用来指向当前节点的前一个节点。
定义链表节点和对象
和单链表类似,多了个pre指向前一个节点,last指向链表最后一个节点
static class ListNode{
public int val;
public ListNode next;
public ListNode pre;
public ListNode (int val) {
this.val = val;
}
}
private ListNode head;
private ListNode last;
实现add方法:插入元素
头插法
因为多了last和pre,所以相较于单链表,需要多考虑last和pre的指向。
//头插法
public void addFirst(int data) {
ListNode list = new ListNode(data);
if(head == null) {
head = list;
last = list;
return;
}
list.next = head;
head.pre = list;
head = list;
}
尾插法
首先判断链表为不为空,若为空从,则新节点就是最后一个节点;若不为空,则直接让last的next指向新节点,再让last等于新的尾节点。
public void addLast(int data) {
ListNode list = new ListNode(data);
if(head == null) {
head = list;
last = list;
return;
}
last.next = list;
list.pre = last;
last = list;
}
指定位置插入
前面和单链表类似,只是到后面更新节点时,cur指的是index位置的前一个节点,方便更新前一个和后一个节点的指向。
public void addIndex(int index,int data) throws OutOfIndexException {
if(index < 0 || index > this.size()) {
throw new OutOfIndexException("下标错误");
}
if(index == 1) {
addFirst(data);
return;
}
if(index == this.size()) {
addLast(data);
return;
}
ListNode list = new ListNode(data);
ListNode cur = head;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
cur.pre.next = list;
list.pre = cur.pre;
list.next = cur;
cur.pre = list;
}
//得到单链表的长度
public int size() {
if(head == null) {
return 0;
}
int count = 0;
ListNode cur = head;
while (cur != null) {
cur = cur.next;
count++;
}
return count;
}
查找包含关键字key的节点是否存在链表中
和单链表一样,都是从头至尾遍历一遍链表查找key值得节点。
public boolean contains(int key) {
if(head == null) {
return false;
}
ListNode cur = head;
while(cur != null) {
if(cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
删除第一次出现关键字为key的节点
双向链表需要考虑3个点:
1.链表是否为空
2. 出现关键词key的节点是否为头节点
3. 出现关键词key的节点是否为尾节点
public void remove(int key) {
ListNode cur = head;
while(cur != null) {
if(cur.val == key) {
if(cur != head) {
if(cur != last) {
cur.next.pre = cur.pre;
cur.pre.next = cur.next;
}else {
cur.pre.next = null;
last = cur.pre;
}
}else {
cur.next.pre = null;
head = cur.next;
}
return;
}else {
cur = cur.next;
}
}
}
删除所有值为key的节点
和前者类似
public void removeAllKey(int key) {
ListNode cur = head;
while(cur != null) {
if(cur.val == key) {
if(cur != head) {
if(cur != last) {
cur.next.pre = cur.pre;
cur.pre.next = cur.next;
}else {
cur.pre.next = null;
last = cur.pre;
}
}else {
if(head.next != null) {
cur.next.pre = null;
}
head = cur.next;
}
}
cur = cur.next;
}
}
打印链表
这里和单链表一样
public void display() {
if(head == null) {
return;
}
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println( );
}
清空链表
这里不可像单链表那样直接,因为每一个节点都可以指向前一个节点和后一个节点,需要置空每一个节点。
public void display() {
if(head == null) {
return;
}
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
System.out.println( );
}