1. 概念
链表是一直链式存储的线性表,所有的元素的内存地址不一定是连续的。
2. 成员变量和接口设计
JDK中的
ArrayList
和LinekdList
都实现了List
接口,这里仅仅为了学习数据结构来撰写,并不涉及到设计层面,因此就没过根据JDK源码来设计实现继承等相关问题。
2.1 成员变量
/**
* 链表长度
*/
private int size;
/**
* 头节点
*/
private Node<E> first;
private static final int ELEMENT_NOT_FOUND = -1;
2.2 节点类
private static class Node<E>{
E element;
Node<E> next;
public Node(E element){
this.element = element;
}
public Node(E element, Node<E> next){
this.element = element;
this.next = next;
}
}
2.2 接口设计
int size()
:返回链表长度;boolean isEmpty()
:判断链表是否为空;boolean contains(E element)
:判断链表中是否存在某元素;void add(E element)
:添加元素;E get(int index)
:返回index位置的元素;E set(int index, E elemnt)
:设置index位置的元素为element;void add(int index, E element)
:向index位置添加元素element;E remove(int index)
:删除index位置的元素;int indexOf(E element)
:查看元素的位置;void clean()
:清空链表。
3. 实现方法
3.1 简便方法实现
int size()
/**
* 获取链表长度
* @return
*/
public int size(){
return size;
}
boolean isEmpty()
/**
* 判断链表是否为空
* @return
*/
public boolean isEmpty(){
return size == 0;
}
void clean()
/**
* 清空链表
*/
public void clean(){
size = 0;
first = null;
}
3.2 获取节点方法
针对链表的操作的方法需要根据节点来进行。
/**
* 返回index位置的节点
* @param index
* @return
*/
private Node<E> node(int index){
rangeCheck(index);
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
3.3 添加
要添加某个元素到指定的位置,找到它前一个节点,并且修改前一个节点的next指向它,再将新节点的next指向原先的下一个节点即可,如下图。
添加前:
添加后:
/**
* 添加元素到index位置
* @param index 位置
* @param e 元素
*/
public void add(int index, E e){
rangeCheckForAdd(index);
if (index == 0){
first = new Node<E>(e,first);
}else {
Node<E> prev = node(index - 1);
prev.next = new Node<E>(e,prev.next);
}
size++;
}
/**
* 添加元素到链表尾部
* @param e
*/
public void add(E e){
add(size,e);
}
3.4 删除
删除时只需要将其前面节点的next指向当前节点的next即可。
/**
* 删除index位置的节点
* @param index
* @return
*/
public E remove(int index){
rangeCheck(index);
Node<E> node = first;
if (index == 0){
first = first.next;
}else {
Node<E> prev = node(index - 1);
node = prev.next;
prev.next = node.next;
}
return node.element;
}
3.5 get set indexOf contains
这四个方法都是基于上方的
node
方法实现的
/**
* 给index位置设置新的值
* @param index
* @param e
* @return
*/
public E set(int index, E e){
Node<E> node = node(index);
E oldE = node.element;
node.element = e;
return oldE;
}
/**
* 返回index位置的元素
* @param index
* @return
*/
public E get(int index){
return node(index).element;
}
/**
* 返回 元素 e 的位置
* @param e
* @return
*/
public int indexOf(E e){
Node<E> node = first;
if (e == null){
for (int i = 0; i < size; i++) {
if (node.element == null){
return i;
}
node = node.next;
}
}else {
for (int i = 0; i < size; i++) {
if (e.equals(node.element)){
return i;
}
node = node.next;
}
}
return ELEMENT_NOT_FOUND;
}
/**
* 判断是否存在该元素
* @param e
* @return
*/
public boolean contains(E e){
return indexOf(e) != ELEMENT_NOT_FOUND;
}
4. 练习
4.1 删除链表中的节点
请编写一个函数,用于 删除单链表中某个特定节点 。在设计函数时需要注意,你无法访问链表的头节点 head ,只能直接访问 要被删除的节点 。
https://leetcode-cn.com/problems/delete-node-in-a-linked-list/
既然找不到当前链表的前驱节点,那么就让后面的节点覆盖他。
public static void deleteNode(ListNode node) {
node.val = node.next.val;
node.next = node.next.next;
}
4.2 反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
https://leetcode-cn.com/problems/reverse-linked-list/
4.2.1 递归实现
public static ListNode2 reverseList(ListNode2 head) {
if (head == null || head.next == null){
return head;
}
ListNode2 newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
4.2.2 迭代实现
public static ListNode2 reverseList2(ListNode2 head) {
if (head == null || head.next == null){
return head;
}
ListNode2 newHead = null;
while (head != null){
ListNode2 tmp = head.next;
head.next = newHead;
newHead = head;
head = tmp;
}
return newHead;
}
4.3 环形链表
给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
https://leetcode-cn.com/problems/linked-list-cycle/
快慢指针
public static boolean hasCycle(ListNode head) {
if (head == null || head.next == null){
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null){
if (slow == fast){
return true;
}
fast = fast.next.next;
slow = slow.next;
}
return false;
}
5. 带虚拟头节点的单向链表
为了方便统一的进行处理,可以添加一个不存储数据只作为头节点来进行指向下一个节点的的节点,称为虚拟头节点。
针对上方普通单向链表进行修改:
- 新增构造函数:因为即使链表中没有真正的节点,也需要一个默认的头节点。
public LinkedListHasHead(){
first = new Node<>(null,null);
}
node()
&toString
:原先是从第一个节点就开始存储数据,但这里第二个才是真正存储数据的节点:
/**
* 返回index位置的节点
* @param index
* @return
*/
private Node<E> node(int index){
rangeCheck(index);
Node<E> node = first.next;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
@Override
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("LinkedList{list = [");
Node<E> node = first.next;
while (node != null){
stringBuilder.append(node.element);
if (node.next != null){
stringBuilder.append(",");
}
node = node.next;
}
stringBuilder.append("]").append(",size = ").append(size).append("}");
return stringBuilder.toString();
}
add()
&remove()
:原先针对往头节点插入数据做了特殊处理,这里因为引入了头节点,无论链表是否为空,都存在有节点的情况,因此可以忽略。
/**
* 添加元素到index位置
* @param index 位置
* @param e 元素
*/
public void add(int index, E e){
rangeCheckForAdd(index);
Node<E> prev = index == 0 ? first : node(index - 1);
prev.next = new Node<E>(e,prev.next);
size++;
}
/**
* 删除index位置的节点
* @param index
* @return
*/
public E remove(int index){
rangeCheck(index);
Node<E> prev = index == 0 ? first : node(index - 1);
Node<E> node = prev.next;
prev.next = node.next;
size--;
return node.element;
}