1、链表定义:
链表通过指针将不连续的内存块串起来,构成的一种数据结构。单向链表、双向链表,双端链表、单向循环、双向循环、跳跃链表等原理也都是基于指针将不连续的内存快串起来。只是指针指向复杂度不一样,提供的各种的API而已。
/**
* 单链表的表示方式:
*/
class Node {
Object element; //节点数据信息:数据域
Node next; //下一个节点的地址(引用)
}
2、链表种类:
单向链表:
private class Node {
// 保存节点的数据
private Object element;
// 指向下一个节点的引用
private Node next;
}
单向循环链表:单向循环链表无非就是最后一个节点的指针指向第一个节点
双向链表、双向循环链表
private class Node {
// 保存节点的数据
private Object data;
// 保存上个节点的引用
private Node prev;
// 指向下一个节点的引用
private Node next;
}
3、单向链表相关方法java实现:
@Data
class Node<T> {
public T data;
public Node next;
//next表示:下一个节点对象(单链表中)
public Node(T data) {
this.data = data;
}
}
/**
*单向链表java实现类
*/
public class MyLinkList<T>{
/**
* 当前结点对象
*/
Node head;
/**
* 当前结点对象
*/
Node current;
/**
* 插入指定位置元素
*
* @param index
* @param obj
*/
public void add(int index, T obj) {
Node<T> node = head;
for (int i = 1; i < index; i++) {
node = node.next;
}
Node newNode = new Node(obj);
Node temp = node.next;
node.next = newNode;
newNode.next = temp;
}
/**
* 在链表末端插入元素
*
* @param obj
*/
public void add(T obj) {
if (head == null) {
Node newNode = new Node(obj);
current = head = newNode;
return;
}
if (current.next != null) {
current = current.next;
add(obj);
return;
}
Node newNode = new Node(obj);
current.next = newNode;
}
/**
* 根据下标删除元素
*
* @param index
*/
public void remove(int index) {
Node<T> node = head;
for (int i = 0; i < index - 1; i++) {
node = node.next;
}
node.next = node.next.next;
}
/**
* 根据特定元素删除(删除第一个)
*
* @param data
*/
public void remove(T data) {
if (head.next == null) {
return;
}
if (head.next.data.equals(data)) {
head.next = head.next.next;
} else {
remove(data);
}
}
/**
* 获取指定位置的元素
*
* @param index
* @return
*/
public T get(int index) {
Node<T> node = head;
for (int i = 0; i <= index - 1; i++) {
node = node.next;
}
return node.data;
}
/**
* 打印链表
*/
public void print() {
String str = "";
Node<T> node = head;
while (node.next != null) {
str += node.data + "->";
node = node.next;
}
str += node.data;
System.err.println(str);
}
}
4、链表的时间复杂度分析:
链表是通过指针将不连续的内存块串起来。链表的单纯插入和删除很快,我们只需要改变一些指针的指向就行了 ,所以就插入和删除时间复杂度为O(1),但是在删除元素之前需要先查找到元素。
通过下标和特定元素查询数据,都需要从链表的第一个或最后一个数据项开始查询。查询的最好时间复杂度为O(1)(元素在链表头),查询的最坏时间复杂度为O(n)(元素在链表尾)。根据时间复杂度加法原则,链表的插入和删除的时间复杂度为O(n)。
5、链表的练习:
常见链表问题:(这里只提供思路,就不写代码了)
(1)链表中环的检测:
利用快、慢指针解法。慢指针,一次遍历一个节点。快指针,一次遍历两个节点。如果在某一次遍历中相遇。则检测到环
(2)删除链表倒数第 n 个结点:
利用快、慢指针解法。让快指针先走N步,再让两个在指针同时后移,直到快指针到达尾部,此时,慢指针的下一个节点就是要被删除的节点了。
(3)求链表的中间结点
利用快、慢指针解法。慢指针一次遍历一个节点,快指针一次遍历两个节点,由于快指针的速度是慢指针的两倍,所以当快指针遍历完链表时,慢指针所处的节点就是链表的中间节点。
(4)单链表反转:
public Node reverse(Node head) {
if (head == null || head.next == null)
return head;
Node temp = head.next;
Node newHead = reverse(head.next);
temp.next = head;
head.next = null;
return newHead;
}
(5)单链表反向打印:(不改变链表结构)
从头到尾打印链表,所谓的“后进先出”,这时候就可以联想到栈这个数据结构。但是会开辟新的内存空间。想到栈就要想到递归
6、链表的应用
- 可以基于链表实现栈、队列、树
- Redis的数据类型Sorted Set(有序集合),底层编码实现ziplist和skiplist,skiplist底层就是跳跃链表+hash表
- java并发容器:AbstractQueuedSynchronizer 底层。当前线程调用lock()方法是获取独占式锁,获取失败就将当前线程加入同步队列。此时的同步队列为双向队列
/* <pre>
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
* </pre>
*/
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
//省略部分代码
}