大纲图
双向链表
Algorithms_基础数据结构(02)_链表&链表的应用案例之单向链表中梳理了 单向链表的基本操作,接下来我们继续来看下双向链表吧。
双向链表的基本结构
单向链表只有一个方向,结点只有一个后继指针next指向后面的结点。
双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点。
双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。
虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。那相比单链表,双向链表适合解决哪种问题呢?
-----> B+Tree:Mysql索引 叶子节点 双向链表
双向链表的基本操作
头插
尾插
中间部位插入
删除头部
删除尾部
删除中间位置的数据
查找
通常,双向链表同单链表一样,都仅有一个头指针。所以双链表查找指定元素的实现同单链表类似,都是从表头依次遍历表中元素,直到找到对应的元素为止。
更新
更改双链表中指定结点数据域的操作那必须要先查找到该节点,因此是在查询的基础上完成的。------>即:通过遍历找到存储有该数据元素的结点,直接更改其数据域即可。
Code
/**
* @author 小工匠
* @version v1.0
* @create 2020-01-03 06:08
* @motto show me the code ,change the word
* @blog https://artisan.blog.csdn.net/
* @description
**/
public class ArtisanDoubleLinkedList {
private ArtisanNode head; // head节点
private ArtisanNode tail; // tail节点 为了方便直接获取tail节点,省去每一次都要遍历的操作
private int size; // 链表元素数量
/**
* 双向链表初始化
*/
public ArtisanDoubleLinkedList() {
this.head = null;
this.tail = null;
}
/**
* 头插
* @param data
*/
public void add2Head(Object data) {
ArtisanNode node = new ArtisanNode(data); // 新的Node
if (this.head == null) { // 如果head节点为null, head和tail节点均为这个新的node节点
this.tail = node;
} else {// 将原来的头节点的前驱节点指向node, 将新节点的后驱节点指向head
this.head.pre = node;
node.next = head;
}
this.head = node; // 将新的节点置为head节点
size++;
}
/**
* 尾插 (低效)
*
* @param data
*/
public void add2Tail(Object data) {// 从头部遍历,找到最后的节点,然后加到尾部
ArtisanNode node = new ArtisanNode(data); // 要加入的节点
ArtisanNode currentNode = head;
if (currentNode == null){
add2Head(data);
}
while(currentNode !=null){
if (currentNode.next == null){ // 说明找到了当前的tail节点
currentNode.next = node ;// 将当前tail节点的next指针指向新的tail节点
node.pre = currentNode; //新的tail节点的pre指向当前tail节点节点
this.tail = node;
break;
}else{
currentNode = currentNode.next;
}
}
size++;
}
/**
* 尾插 (利用tail 无需遍历 效率更高)
*
* @param data
*/
public void add2Tail2(Object data) {// 已经设置tail了,直接用即可,效率更高
ArtisanNode node = new ArtisanNode(data); // 要加入的节点
if (this.head == null){
add2Head(data);
}else {
tail.next = node;
node.pre = tail;
tail = node;
}
}
/**
*
* @param postition
* @param data
*/
public void add2Nth(int postition ,Object data) {
ArtisanNode newNode = new ArtisanNode(data); // 新的Node
ArtisanNode currentNode = head;
if (postition == 0 ){ // 如果是0 ,添加到头节点
add2Head(data);
}else {
for (int i = 1; i < postition; i++) { // 找到要插入的位置的前面的节点
currentNode = currentNode.next;
}
// 与后继节点建立双层逻辑关系
newNode.next = currentNode.next;
currentNode.next.pre = newNode;
// 与前置节点建立双层逻辑关系
currentNode.next = newNode;
newNode.pre = currentNode;
}
size++;
}
/**
* 根据value 查找元素
* @param data
* @return
*/
public ArtisanNode find(Object data){ // 从头遍历
ArtisanNode currentNode = head;
while(currentNode != null){
if (data.equals(currentNode.data)){
printPreAndNextInfo(currentNode);
break;
}else{
currentNode = currentNode.next;
}
}
return currentNode;
}
/**
* 删除头部节点
*/
public void deleteHead(){
this.head = this.head.next; // 将当前头节点的下一个节点置为头节点
this.head.pre = null; // 将前置节点置为null
size--;
}
/**
* 删除尾部节点
*/
public void deleteTail(){
ArtisanNode currentNode = this.head;
ArtisanNode previousNode = null;
while (currentNode != null){
if (currentNode.next == null){
currentNode.pre = null;// 最后一个节点的pre置为置为null
previousNode.next = null;// 前置节点的next指针置为null
this.tail = previousNode; // 将当前节点的前一个节点置为tail节点
}else { // 如果当前节点的next指针指向不为空,则把下个节点置为当前节点,继续遍历
previousNode = currentNode;// 保存上一个节点的信息
currentNode = currentNode.next;
}
}
}
/**
* 删除指定位置的节点
* @param position
*/
public ArtisanNode deleteNth(int position){
ArtisanNode currentNode = this.head;
if (position == 0 ){
deleteHead();
}else {
for (int i = 1 ; i < position ; i++){// 找到要删除节点的前一个节点
currentNode = currentNode.next;
}
currentNode.next.next.pre = currentNode; // 将 要删除节点的后一个节点的前驱节点 指向 当前节点(要删除的节点的前一个节点)
currentNode.next = currentNode.next.next; // 将 要删除节点的前一个节点的next指针指向 要删除节点的后一个节点
}
size--;
return currentNode.next ; // 返回删除的节点
}
/**
* 获取tail节点
* @return tail节点
*/
public ArtisanNode getTail(){
System.out.println("tail节点的值为:" + this.tail.data );
return this.tail;
}
/**
* 获取head节点
* @return head节点
*/
public ArtisanNode getHead(){
System.out.println("head节点的值为:" + this.head.data );
return this.head;
}
/**
* 打印链表中的数据
*/
public void print() {
ArtisanNode currentNode = this.head;// 从head节点开始遍历
while (currentNode != null) { // 循环,节点不为空 输出当前节点的数据
System.out.print(currentNode.data + " -> ");
currentNode = currentNode.next; // 将当前节点移动到下一个节点,循环直到为null
}
System.out.print("null");
System.out.println();
}
/**
* 打印前后节点信息
* @param currentNode
*/
private void printPreAndNextInfo(ArtisanNode currentNode) {
System.out.println("当前节点:" + currentNode.data);
if (currentNode.pre != null){
System.out.println("当前节点【" + currentNode.data + "】的前驱节点:" + currentNode.pre.data);
}else{
System.out.println("当前节点【"+ currentNode.data + "】为head节点");
}
if (currentNode.next != null){
System.out.println("当前节点【"+ currentNode.data + "】的后继节点:" + currentNode.next.data);
}else{
System.out.println("当前节点【"+ currentNode.data + "】为tail节点");
}
}
public static void main(String[] args) {
ArtisanDoubleLinkedList doubleLinkedList = new ArtisanDoubleLinkedList();
doubleLinkedList.add2Head("artisanData96");
doubleLinkedList.add2Head("artisanData97");
doubleLinkedList.add2Head("artisanData99");
doubleLinkedList.add2Head("artisanData98");
doubleLinkedList.getTail();
doubleLinkedList.add2Tail("artisanData100");
doubleLinkedList.getTail();
doubleLinkedList.print();
doubleLinkedList.getHead();
// doubleLinkedList.add2Nth(2,"addedDataByPos");
// doubleLinkedList.add2Tail2(1);
// doubleLinkedList.add2Tail2(2);
// doubleLinkedList.add2Tail2(3);
// doubleLinkedList.add2Tail2(4);
// doubleLinkedList.print();
//
// System.out.println("tail:" + doubleLinkedList.tail.data);
//
// doubleLinkedList.find("artisanData98");
// doubleLinkedList.deleteHead();
// doubleLinkedList.print();
// doubleLinkedList.find("artisanData99");
// System.out.println("被删除节点:" + doubleLinkedList.deleteNth(1).data);
// doubleLinkedList.print();
// doubleLinkedList.find("artisanData96");
}
/**
* 双向链表中的节点
*/
class ArtisanNode {
ArtisanNode pre; // 前驱结点
Object data; // 数据
ArtisanNode next;// 后继节点
public ArtisanNode(Object data) {
this.data = data;
}
}
}
总结
重要区别:
-
1.数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。
-
2.链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读。
-
3.数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它, 导致“内存不足(out ofmemory)”。如果声明的数组过小,则可能出现不够用的情况。注意下标越界的问题。
-
4.动态扩容:数组需再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,使用的时候也需要考虑占用内存的问题。