本节讲述的是单链表与双链表基础方法的原理及实现,双链表将在单链表的基础之上进行讲解,并附有代码。
谢谢关注
链表:是一种物理存储单元上非连续、非顺序的存储结构,它既可以表示线性结构,也可以用于表示非线性结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
由上图单链表结构可知,单链表的每一个节点里,包含一个数据域和一个指针域,数据域指该节点的元素,而指针域则指向下一个节点的地址。
代码将展示节点的定义及其初始化
//定义节点类
private class Node{
public T e;
public Node next;
public Node(T e,Node next){
this.e=e;
this.next=next;
}
public Node(T e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString(){
return e.toString();
}
}
这是数据结构中两个最基础的函数,前者的作用是获取链表的长度,而后者是布尔类型,用于判断链表是否为空
代码如下
//返回链表中元素个数
public int getSize(){
return size;
}
//判断链表是否为空
public boolean isEmpty(){
return size==0;
}
在链表中,我们经常需要进行读取操作,则需要从第一个元素遍历,但会遇到逻辑不够清晰的情况,所以将在这里介绍虚拟头结点,使得链表的逻辑更加清晰
这是原本链表头结点
这是链表的虚拟头结点
可以看到,虚拟头结点是链表头结点的前置节点
且虚拟头结点有两个特点->1.虚拟头结点数据域为空->2.虚拟头结点指向链表的头结点。
这里还包含对size的定义及初始化
//虚拟头结点
private Node dummyHead;
private int size;//定义了一个私有的变量size
//构造函数
public Linkedlist(){
dummyHead=new Node(null,null);
size=0;
}
注意:博主找不到合适的图了,上图head的正确处理与下图左边类似
首先,我们需要定义一个index,也就是索引,索引处则是我们要插入节点的位置
第二,我们需要遍历找到索引处的前一个节点,而虚拟头结点的作用即是为了方便我们在链表中逻辑清晰的遍历节点
这里给出代码及注释,希望读着朋友反复体会
Node prev = dummyHead;//使prev为虚拟头结点,即头结点的前置节点
for(int i=0;i<index;i++){//一共循环index次
prev=prev.next;//prev等于prev的下一个元素,也就是说prev在往后遍历。一直到index-1
//(接上)的位置,因为有一个虚拟头结点所以是index-1
}
第三,讲解逻辑:我们定义node即为要插入index位置的节点
node.next=prev.next实际上是使得新添加进去的节点node的后一位等于node前一个节点prev的后一个
prev.next=node的意思是prev所指向的节点(即后一个节点)为node,node在prev的后面
第四,维护链表,size++;
具体代码
//向链表中添加元素
public void add(int index,T e){
if(index<0||index>size)
throw new IllegalArgumentException("index is illegal");
Node prev = dummyHead;//使prev为虚拟头结点,即头结点的前置节点
for(int i=0;i<index;i++){//一共循环index次
prev=prev.next;//prev等于prev的下一个元素,也就是说prev在往后遍历。一直到index-1
//(接上)的位置,因为有一个虚拟头结点所以是index-1
}
/*Node node = new Node(e);
*node.next=prev.next;
prev.next=node;*/
prev.next = new Node(e,prev.next);//其实上述逻辑相当于这一行代码,希望读者朋友细细品味
size ++;
}
首先,定义index,是要删除节点的位置
第二,定义prev,从dummyHead开始遍历,找到删除节点node的前一个节点
第三,逻辑:先声明node的位置,是在prev的后面
再使得prev的下一个元素等于node的下一个节点
最后使得node的下一个节点为空
第四,维护size,size–;
具体代码如下
public T remove(int index){
if(index<0||index>=size)
throw new IllegalArgumentException("index is illegal");
Node prev = dummyHead;
for(int i=0;i<index;i++){
prev = prev.next;
}
Node retNode = prev.next;
prev.next = retNode.next;
retNode.next = null;
size --;
return retNode.e;
}
实际上就是链表的简单遍历,原理与上述添加元素与删除元素的原理类似,只是这会找的不是前一个节点,而是直接找到dummyHead处的节点,所以从第一个节点开始,于是就有了Node prev=dummyHead.next;
代码如下
public T get(int index){
if(index<0||index>=size)
throw new IllegalArgumentException("index is illegal");
Node cur = dummyHead.next;
int i=0;
while(i<index){
cur=cur.next;
i++;
}
return cur.e;
}
运用布尔类型,遍历原理与上述原理类似
public boolean contains(T e){
for(Node cur = dummyHead.next;cur!=null;cur=cur.next){
if(cur.e.equals(e))
return true;
}
return false;
}
这里需要注意的是equals,是指e的内容与前面e的内容相同,而非字符串
遍历方式与上类似,并不赘述
代码如下
public T get(int index){
if(index<0||index>=size)
throw new IllegalArgumentException("index is illegal");
Node cur = dummyHead.next;
int i=0;
while(i<index){
cur=cur.next;
i++;
}
return cur.e;
}
利用StringBuilder,这个类可以运用append在代码后面添加内容,与StringBuffer类似
代码中遍历与上述类似
代码如下
@Override
public String toString(){
StringBuilder res=new StringBuilder();
Node cur = dummyHead.next;
for(int i=0;i<size;i++){
res.append(cur);
res.append("->");
cur=cur.next;
}
res.append("NULL");
return res.toString();
}
public class Linkedlist<T> {
//定义节点类
private class Node{
public T e;
public Node next;
public Node(T e,Node next){
this.e=e;
this.next=next;
}
public Node(T e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString(){
return e.toString();
}
}
//虚拟头结点
private Node dummyHead;
private int size;//定义了一个私有的变量size
//构造函数
public Linkedlist(){
dummyHead=new Node(null,null);
size=0;
}
//返回链表中元素个数
public int getSize(){
return size;
}
//判断链表是否为空
public boolean isEmpty(){
return size==0;
}
//向链表中添加元素
public void add(int index,T e){
if(index<0||index>size)
throw new IllegalArgumentException("index is illegal");
Node prev = dummyHead;//使prev为虚拟头结点,即头结点的前置节点
for(int i=0;i<index;i++){//一共循环index次
prev=prev.next;//prev等于prev的下一个元素,也就是说prev在往后遍历。一直到index-1
//(接上)的位置,因为有一个虚拟头结点所以是index-1
}
/*Node node = new Node(e);
*node.next=prev.next;
prev.next=node;*/
prev.next = new Node(e,prev.next);
size ++;
}
public void addFirst(T e){
add(0,e);
}
public void addLast(T e){
add(size,e);
}
public T get(int index){
if(index<0||index>=size)
throw new IllegalArgumentException("index is illegal");
Node cur = dummyHead.next;
int i=0;
while(i<index){
cur=cur.next;
i++;
}
return cur.e;
}
public T getFirst(){
return get(0);
}
public T getLast(){
return get(size-1);
}
public void set(int index,T e){
if(index<0||index>=size)
throw new IllegalArgumentException("index is illegal");
Node cur=dummyHead.next;
for(int i=0;i<index;i++){
cur=cur.next;
}
cur.e=e;
}
public boolean contains(T e){
for(Node cur = dummyHead.next;cur!=null;cur=cur.next){
if(cur.e.equals(e))
return true;
}
return false;
}
public T remove(int index){
if(index<0||index>=size)
throw new IllegalArgumentException("index is illegal");
Node prev = dummyHead;
for(int i=0;i<index;i++){
prev = prev.next;
}
Node retNode = prev.next;
prev.next = retNode.next;
retNode.next = null;
size --;
return retNode.e;
}
public T removeFirst(){
return remove(0);
}
public T removeLast(){
return remove(size-1);
}
@Override
public String toString(){
StringBuilder res=new StringBuilder();
Node cur = dummyHead.next;
for(int i=0;i<size;i++){
res.append(cur);
res.append("->");
cur=cur.next;
}
res.append("NULL");
return res.toString();
}
}
双链表与单链表的逻辑十分相似。不同之处在于双链表有两个指针域,一个指向上一个节点,一个指向下一个节点。这里主要讲述的是双链表的结构,成员变量,以及与单链表的不同之处。
这里需要强调的是:每一个节点的后指针指向下一个节点的数据域,而前指针指向上一个节点的数据域。箭头都是由指针指向数据域的。
博主定义了一个E e(数据),一个nextdown(左边的指针),一个nextup(右边的指针),希望不要给读着带来误解
public class Node{
//三个成员变量
public E e;
public Node nextup;//node的下一个节点
public Node nextdown;//node的上一个节点
//初始化
Node(Node nextup,E e,Node nextdown){
this.e=e;
this.nextup=nextup;
this.nextdown=nextdown;
}
Node(E e){
this.e=e;
this.nextup=null;
this.nextdown=null;
}
@Override
public String toString(){
return e.toString();
}
}
基础方法之中,与单链表不同得,只有双链表的增添与删减。
添加元素不同之处:单链表是将节点添加到该链表的最后一个节点之后,将最后一个节点的next指向添加的节点即可。而双链表也是将节点添加到最后一个节点之后,只不过还要将添加节点的前驱指向最后一个节点,实现双向链接。
这里附上代码
public void add(int index,E e){
if(index<0||index>size){
throw new IllegalArgumentException("index is illegal");
Node prev=FirstNode;
for(int i=0;i<index;i++)
prev.nextup=prev;//让prev这个节点挪在要加进去的前一个节点的位置
Node node=new Node(e);
node.nextdown=prev.nextup;//node的上一个节点指向prev的下一个节点
prev.nextup=node.nextdown;//prev的下一个节点是node的上一个节点;
size++;
}
}
删除元素的不同之处:删除有点特殊,单链表是将一个节点移除之后,将该节点的前一个节点的next指向该节点的下一个节点。但是双链表有点不同,除了将一个方向的指针移动之外,还要操作反方向的指针,即将删除节点的下一个节点的前驱指向删除节点的上一个节点,即完成删除。
代码如下
public E remove(int index){
if(index<0||index>size)
throw new IllegalArgumentException("index is Illegal");
//先找到索引前一个的元素
Node prev=FirstHead;
for(int i=0;i<index;i++){
prev=prev.nextup;
}
Node node=new Node(find(index));//要删除的那个节点
node.nextdown=prev.nextup;//声明node的所在位置,prev是node的前一个节点
prev.nextup=node.nextup;//prev的下一个节点是node的下一个节点、
node.nextup=null;
size--;
return find(index);//返回删除的那个元素
}
public class Mydlinked<E> {
public class Node{
//三个成员变量
public E e;
public Node nextup;//node的下一个节点
public Node nextdown;//node的上一个节点
//初始化
Node(Node nextup,E e,Node nextdown){
this.e=e;
this.nextup=nextup;
this.nextdown=nextdown;
}
Node(E e){
this.e=e;
this.nextup=null;
this.nextdown=null;
}
@Override
public String toString(){
return e.toString();
}
}
private int size;
//创建一个头结点
private Node FirstHead=new Node(null,null,null);//初始化虚拟头结点
//获取链表长度
public int getSize(){
return size;
}
//判断链表是否为空
public boolean isEmtpy(){
return size==0;
}
//向链表中添加元素
//添加操作也有点不同,之前单链表是将节点添加到该链表的最后一个节点之后,将最后一个节点的next指向添加的节点即可。
//双链表也是将节点添加到最后一个节点之后,只不过还要将添加节点的前驱指向最后一个节点,实现双向链接。
public void add(int index,E e){
if(index<0||index>size){
throw new IllegalArgumentException("index is illegal");
Node prev=FirstNode;
for(int i=0;i<index;i++)
prev.nextup=prev;//让prev这个节点挪在要加进去的前一个节点的位置
Node node=new Node(e);
node.nextdown=prev.nextup;//node的上一个节点指向prev的下一个节点
prev.nextup=node.nextdown;//prev的下一个节点是node的上一个节点;
size++;
}
}
public void addFirst(E e){
add(0,e);
}
//查找索引处的元素并返回该元素
public E find(int index){
if(index<0||index>size)
throw new IllegalArgumentException("index is Illegal");
Node prev=FirstHead.nextup;
for(int i=0;i<index;i++)
prev=prev.nextup;
return prev.nextup.e;
}
//查找是否存在元素e
public boolean finde(E e){
Node prev=FirstHead.nextup;
for(int i=0;i<size;i++){
prev=prev.nextup;
if(prev.e.equals(e))
return true;
}
return false;
}
//删除索引处的元素
//删除有点特殊,单链表是将一个节点移除之后,将该节点的前一个节点的next指向该节点的下一个节点。
//但是双链表有点不同,除了将一个方向的指针移动之外,还要操作反方向的指针
//即将删除节点的下一个节点的前驱指向删除节点的上一个节点,即完成删除。
public E remove(int index){
if(index<0||index>size)
throw new IllegalArgumentException("index is Illegal");
//先找到索引前一个的元素
Node prev=FirstHead;
for(int i=0;i<index;i++){
prev=prev.nextup;
}
Node node=new Node(find(index));//要删除的那个节点
node.nextdown=prev.nextup;//声明node的所在位置,prev是node的前一个节点
prev.nextup=node.nextup;//prev的下一个节点是node的下一个节点、
node.nextup=null;
size--;
return find(index);//返回删除的那个元素
}
//打印
public String toString(){
if(size==0)
throw new IllegalArgumentException("size is Illegal");
StringBuilder res=new StringBuilder();
Node prev=FirstHead.nextup;
for(int i=0;i<size;i++){
prev=prev.nextup;
res.append(prev.e);
res.append("->");
}
return res.toString();
}
}
链表是最基础的数据结构之一,我们同样也可以利用链表实现栈和队列,让其他两种数据结构实现真正的动态。在以后的学习中也有许多用途。
谢谢大家关注!