一文多图搞懂数据结构的双链表!

  • 2019 年 10 月 6 日
  • 筆記

双向链表

前面讲过线性表中[顺序表和链表].但双向链表无论在考察还是运用中都占有很大的比例,笔者旨在通过本文与读者一起学习分享双链表相关知识。

双链表介绍


与单链表区别

逻辑上没有区别。他们均是完成线性表的内容。主要的区别是结构上的构造有所区别。 对于单链表:

  • 对于一个节点,有储存数据的data。和next后驱节点(指针)。也就是这个单链表想要一些遍历的操作都得通过前节点—>后节点

对于双链表:

  • 对于一个节点,有些和单链表一样有存储数据的data,指向后方的next(指针)。它拥有单链表的所有操作和内容。但是他还有一个前驱节点pre(指针)。

结构的设计

  • 对于双链表的结构,上图也很清楚的。以前设计的单链表是带头节点的。带头节点可以方面首位的插入和删除。而这次我们抱着学习的态度搞清链表故该双链表是不带头节点的.
  • 同时,以前的单链表是不带尾节点的,这次我们带上尾节点tail。这样我们就接触了几乎所有类型啦!遇到啥也不怕了。

所以我们构造的这个双链表的的性质:

  • 不带头节点、带尾指针(tail)、双向链表。

对于node节点:

class node<T> {      T data;  	node<T> pre;  	node<T> next;    	public node() {  	}    	public node(T data) {  		this.data = data;  	}  }  

对于链表:

public class doubleList<T> {      private node<T> head;// 头节点  	private node<T> tail;// 尾节点  	private int length;      //各种方法  }  

具体方法的解析

  • 其实对于一个链表主要的操作还是增删。增闪的话都需要考虑是否带头节点。头插尾插中间插。并且还要考虑其中的一些细节处理。指针的运算。防止链表崩掉。因为这些操作如果不当往往会对链表的结构和证悟性带来致命的打击。而像查找那些逻辑稍微简单。也很容易排查错误。

初始化

  • 我们知道一个双链表在最初的时候它的数据肯定是为null的。那么对于这个不带头节点的双链表而言。它的head始终指向第一个真实有效的数据。tail也是如此。那么在最初没数据的时候当然要head=null,并且tail=head。(tail和head需要在一个链上)。
public doubleList() {  	head = null;  	tail = head;  	length = 0;  	}  

增加

空表插入:

  • 对于空链表来说。增加第一个元素可以特殊考虑。因为在链表为空的时候headtail均为null。但head和tail又需要实实在在指向链表中的真实数据(带头指针就不需要考虑)。所以这时候就新建一个node让head、tail等于它。
node<T> teamNode = new node(data);  if (isEmpty()) {  	head = teamNode;  	tail = teamNode;  }  

头插入:

对于头插入来说。步骤很简单,只需考虑head节点的变化。

  1. 新建插入节点node
  2. head前驱指向node
  3. node后驱指向head
  4. head指向node。(这时候head只是表示第二个节点,而head需要表示第一个节点故重新赋值)

尾插入:

对于尾插入来说。只需考虑尾节点tail节点的变化。

  1. 新建插入节点node
  2. node前驱指向tail
  3. tail后驱指向node
  4. tail指向node。(这时候tail只是表示倒数第二个节点,而tail需要表示最后节点故重新赋值等于node即可)

编号插入:

对于编号插入来说。要考虑查找和插入两部,而插入既和head无关也和tail无关。

  1. 新建插入节点node
  2. 找到欲插入node的前一个节点pre。和后一个节点after
  3. node后驱指向after,after前驱指向node(次时node和后面节点的关联已经完成,但是和前面处理分离状态)
  1. pre后驱指向node。node前驱指向pre(此时完毕)

整个流程的动态图为:

删除

单节点删除:

无论头删还是尾删,遇到单节点删除的需要将链表从新初始化!

if (length == 1)// 只有一个元素  {  	head = null;  	tail = head;  	length--;  }  

头删除:

头删除需要注意的就是删除不为空时候头删除只和head节点有关

大致分为:

  1. head节点的后驱节点的前驱节点改为null。(head后面本指向head但是要删除第一个先让后面那个和head断绝关系)
  2. head节点指向head.next.(这样head就指向我们需要的第一个节点了。如果有需要处理内存的语言就可以把第一个被孤立的节点删除了)

尾删除:

尾删除需要注意的就是删除不为空时候尾删除只和tail节点有关。记得在普通链表中,我们删除尾节点需要找到尾节点的前驱节点。需要遍历整个表。而双向链表可以直接从尾节点遍历到前面。

删除的时tail所在位置的点。也就是tail所在节点要断绝和双链表的关系。

  1. tail.pre.next=null尾节点的前一个节点(pre)的后驱节点等于null
  2. tail=tail.pre尾节点指向它的前驱节点,此时尾节点由于步骤1next已经为null。完成删除

普通删除:

普通删除需要重点掌握,因为前两个删除都是普通删除的一个特例而已。(普通删除要确保不是头删除和尾删除)

  1. 找打将删除节点的前驱节点team(team.next是要删除的节点)
  2. team.next.next.pre=team.(欲被删除节点的后一个节点的前驱指向team,双向链表需要处理pre和next。这步处理了pre)
  1. team.next=team.next.next;此时team.next也跳过被删除节点。
  1. 完成删除

整个流程的动态图为:

代码与测试


代码:

package LinerList;    /*   * 不带头节点的   */  public class doubleList<T> {  class node<T> {  	T data;  	node<T> pre;  	node<T> next;    	public node() {  	}    	public node(T data) {  		this.data = data;  	}  }    private node<T> head;// 头节点  private node<T> tail;// 尾节点  private int length;    public doubleList() {  	head = null;  	tail = head;  	length = 0;  }    boolean isEmpty() {  	return length == 0 ? true : false;  }    void addfirst(T data) {  	node<T> teamNode = new node(data);  	if (isEmpty()) {  		head = teamNode;  		tail = teamNode;    	} else {  		teamNode.next = head;  		head = teamNode;  	}  	length++;  }    void add(T data)// 尾节点插入  {  	node<T> teamNode = new node(data);  	if (isEmpty()) {  		head = teamNode;  		tail = teamNode;  	} else {  		tail.next = teamNode;  		teamNode.pre=tail;  		tail = teamNode;  	}  	length++;  }     int length()     {     	return length;     }  T getElum(int index)//为了简单统一从头找  {  	node<T> team=head;  	for(int i=0;i<index;i++)//不带头节点  遍历次数-1  	{  		team=team.next;  	}  	return team.data;  }  void add(int index, T data)// 编号插入  {  	if (index == 0) {  		addfirst(data);  	} else if (index == length) {  		add(data);  	} else {// 重头戏  		node teampre = head;// 为插入的前qu  		for (int i = 0; i < index -1; i++)// 无头节点,index-1位置找到前驱节点  		{  			teampre = teampre.next;  		}    		node<T> team = new node(data);// a c 中插入B 找打a  		team.next = teampre.next;// B.next=c  		teampre.next.pre = team;// c.pre=B  		team.pre = teampre;// 关联a B  		teampre.next = team;  		length++;  	}  }  void deletefirst()// 头部删除  {  	if (length == 1)// 只有一个元素  	{  		head = null;  		tail = head;  		length--;  	} else {  		head = head.next;  		length--;  	}  }   void deletelast() {  	if(length==1)  	{  		head=null;  		tail=head;  		length--;  	}  	else {    		tail.pre.next=null;  		tail=tail.pre;  		length--;    	}  }   void delete(int index)   {  	 if(index==0)deletefirst();  	 else if (index==length-1) {  		deletelast();  	}  	 else {//删除 为了理解统一从头找那个节点  		node<T>team=head;  		for(int i=0;i<index-1;i++)  		{  			team=team.next;  		}  		//team 此时为要删除的前节点  a  c   插入B  a为team  		team.next.next.pre=team;//c的前驱变成a  		team.next=team.next.next;//a的后驱变成c  		length--;  	}   }     void set(int index,T data)     {     	node<T>team=head;  	for(int i=0;i<index-1;i++)  	{  		team=team.next;  	}  	team.data=data;     }  @Override  public String toString() {  	node<T> team = head;  	String vaString = "";  	while (team != null) {  		vaString += team.data + " ";  		team = team.next;  	}  	return vaString;  }  }  

测试:

总结与感悟

插入、删除顺序问题:

  • 很多人其实不清楚插入、删除正确的顺序是什么。其实这点没有必然的顺序,要根据题意所给的条件完成相同的结果即可!
  • 还有就是你可能会搞不清一堆next.next这些问题。这时候建议你画个图。你也可以先建一个节点,用变量名完成操作,可能会更容易一些。比如删除操作,你找到pre节点(删除前的节点)。你可以node delete=pre.next,node next=delete.next。这样你直接操作pre。delete。next三个节点会更简单。
  • 但是很多题目只给你一个node。你这时候要分析next(pre)。改变顺序。因为只有一个节点,你改变next(pre)很可能导致你遍历不到那个节点。所以这种情况要好好思考(可以参考笔者的代码实现)。
  • 至于有些语言需要删除内存的。别忘记删除。(java大法好)

其他操作问题:

  • 对于其他操作,相比增删要容易理解,可以参考代码理解。
  • 双向链表可以对很多操作进行优化。这里只是突出实现并没有写的太多。比如查找时候可以根据长度判断这个链表从头查找还是从尾查找

另外,代码写的可能不是太好,链表也没考虑线程安全问题。算法效率可能不太优。如果有什么改进或者漏洞还请大佬指出!