单向链表基本介绍
链表是一种数据结构,和数组同级。比如,Java中我们使用的ArrayList,其实现原理是数组。而LinkedList的实现原理就是链表了。链表在进行循环遍历时效率不高,但是插入和删除时优势明显。下面对单向链表做一个介绍。
单向链表是一种线性表,实际上是由节点(Node)组成的,一个链表拥有不定数量的节点。其数据在内存中存储是不连续的,它存储的数据分散在内存中,每个结点只能也只有它能知道下一个结点的存储位置。由N各节点(Node)组成单向链表,每一个Node记录本Node的数据及下一个Node。向外暴露的只有一个头节点(Head),我们对链表的所有操作,都是直接或者间接地通过其头节点来进行的。
单链表的操作:
添加:上图可以看出 单向链表只有一个指向,原来head为p,p指向s,添加结点只需要把p指向q,q指向s就可以了,即:p—>q ; q—>s ; 这样就实现了单向链表的添加;
删除:原理与添加相反,若此时链表为 p—> q —>s ; 若删除q节点只需要更改p的指向就可以了 p—>s,这样就删掉了;
查找:查找操作需要对整个但链表进行遍历,直到满足查找条件为止;
修改:此操作一般建立在查找之上进行,找到借点之后对值进行修改。
Node.java
package list;
public class Node {
public Node data;// 数据区
public Node next;// 指针区
public Node(Node data, Node next) {
this.data = data;
this.next = next;
}
public Node() {
}
public void setData(Node data) {
this.data = data;
}
public Node getData() {
return data;
}
public void setNext(Node next) {
this.next = next;
}
public Node getNext() {
return next;
}
}
LinkList.java
package list;
public class LinkList {
public Node head;// 头结点
public int count;// 记录节点的长度
public LinkList() { // 构造函数用来初始化
head = null;
count = 0;
}
// 节点的添加
public void addNode(Node data) {
Node node = new Node(data, null);
Node temp = null;
if (head != null) {
temp = head;
while (temp.getNext() != null) {
temp = temp.getNext();
}
temp.setNext(node);
} else {
head = node;
temp = node;
}
count++;
}
// 节点的删除
public void delNode(Node data) {
Node front = null;// 定义一个空节点,用于接收和判断被删除节点
while (head != null) {
if (head.equals(data)) {
break;
}
front = head;
head = head.getNext();
}
if (head != null) {
if (front == null) {
head = head.getNext();
} else {
front.setNext(head.getNext());
}
count--;
} else {
count--;
}
}
// 给定下标删除节点
public void delNode_count(int index) {
if (index < 0 || index > count - 1) {
System.out.println("链表索引越界");
}
Node temp = head;// 作用同上
// 找到要删除节点的前一个节点
for (int i = 0; i < index - 1; i++) {
temp = temp.getNext();
}
// 找到之后 此节点的前节点和此节点的后节点进行连接
// 让要删除节点的前一个节点,指向被删除节点的后一个节点,也就是指向要删除节点的后后一个节点
temp.setNext(temp.getNext().getNext()); // 把要删除的节点隔过去进行连接,也就是实现了删除节点的操作
// 长度减1
count--;
}
// 以给出的index 查找节点
public Node findNode(int index) {
if (index < 0 || index > count - 1) {
System.out.println("链表索引越界");
}
Node temp = head;
for (int i = 0; i < index - 1; i++) {
// 找到之后获取index在链表中的位置,
// 表示链表中第index个节点的值是temp.getData;
temp = temp.getNext();
}
// 根据需要可返回找到的数据对象,也可不返回,
// 此处建议返回,这样可以把链表封装起来
return temp;
}
// 以对象查找节点
public Node findNode(Node data) {
Node temp = head;
while (temp != null) {
if (temp.equals(data)) {
return temp;
}
temp.setNext(temp.getNext());
}
return null;
}
// 修改
public void updateNode(Node data) {
Node temp = findNode(data);
if (temp != null) {
temp.setData(data);
}
}
// 打印
public void print() {
Node temp = head;
while (temp != null) {
System.out.println(temp.getData().toString());
temp = temp.getNext();
}
}
}
双向列表
如果我们在数组的第一项处插入或者删除元素该操作花费的时间为常数,但是如果我们需要在最后一项插入或者删除元素这是一个花费时间为O(N)的操作。
我们可以用双向链表来解决这个问题。双向链表的每一个结点都有一条指向其后继结点的next链和一条指向其前结点的pre链。双向链表既可以从第一项开始遍历也可以从最后一项开始往前遍历,双向链表可以用下图表示:
增加节点:
删除节点:
双向列表的具体实现如下:
// DoubleLinkList.java
public class DoubleLinkList<T> {
private class Node<T> {
// 节点值
private T value;
// 前一个节点值
private Node<T> prev;
// 后一个节点值
private Node<T> prex;
public Node(T value, Node<T> prev, Node<T> prex) {
this.value = value;
this.prev = prev;
this.prex = prex;
}
}
// 链表长度
private int size;
// 头结点
private Node<T> head;
public DoubleLinkList() {
// 头结点不存储值,并且头结点初始化时,就一个头结点
// 所以头结点的前后节点都是自己
// 并且这个链表的长度是0
head = new Node<>(null, null, null);
head.prev = head.prex;
head = head.prex;
size = 0;
}
public int getSize() {
return this.size;
}
/**
* 判断链表的长度是否为空
*/
public boolean isEmplty() {
return size == 0;
}
// 判断索引是否超出范围
public void checkIndex(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
return;
}
/**
* 通过索引获取链表当中的节点
*
*/
public Node<T> getNode(int index) {
// 检查该索引是否超出范围
checkIndex(index);
/**
* 当索引的值小于该链表长度的一半时,那么从链表的头结点开始向后找是最快的
*
*/
if (index < size / 2) {
Node<T> cur = head.prex;
for (int i = 0; i < index; i++) {
cur = cur.prex;
}
return cur;
}
/**
* 当索引值位于链表的后半段时,则从链表的另一端开始找是最快的
*/
Node<T> cur = head.prev;
int newIndex = size - (index + 1);
for (int i = 0; i < newIndex; i++) {
cur = cur.prev;
}
return cur;
}
/**
* 获取节点当中的值
*/
public T getValue(Node<T> cur) {
return cur.value;
}
/**
* 获取第一个节点的值
*/
public T getFirst() {
return getValue(getNode(0));
}
/**
* 获取最后一个节点的值
*/
public T getLast() {
return getValue(getNode(size - 1));
}
/**
* 插入节点
*/
public void insert(int index, T value) {
// 如果这次插入时,链表是空的
if (index == 0) {
// 这个节点的
Node<T> cur = new Node<T>(value, head, head.prex);
head.prex.prev = cur;
head.prex = cur;
size++;
return;
}
// 先根据给出的插入位置,找到该链表原来在此位置的节点
Node<T> node = getNode(index);
/**
* 放置的位置的前一个节点就是原节点的前置节点 而后节点就是原节点
*/
Node<T> cur = new Node<T>(value, node.prev, node);
/**
* 现将该位置也就是原节点的前节点的后节点,赋值成为新节点 然后将新节点的后置节点的值赋值为原节点
*/
node.prev.prex = cur;
node.prev = cur;
size++;
}
/**
* 向表头插入数据
*/
public void insertTo(T Value) {
insert(0, Value);
}
/**
* 将元素插入到链表的尾部
*/
public void insertTotatil(T value) {
Node<T> cur = new Node<>(value, head.prev, head);
// head.prev 代表原来的尾部节点
/**
* 遵循两个原则:1.新插入节点的前一个节点的后一个节点为新节点 2.新节点的后一个节点的前一个节点为新节点
*/
head.prev.prex = cur;
head.prev = cur;
size++;
}
/**
* 删除节点的方法
*/
public void del(int index) {
checkIndex(index);
Node<T> cur = getNode(index);
// 记住此时的指针还没断开,赋值以后才相当于断开
cur.prev.prex = cur.prex;
cur.prex.prev = cur.prev;
size--;
cur = null;
return;
}
/**
* 删除第一个节点
*/
public void delFirst() {
del(0);
}
/**
* 删除最后一个节点
*/
public void delLast() {
del(size - 1);
}
}