动态链表:
在学过数组后,发现数组的存储结构有一定的缺陷。就是增(插入位置)、删、改效率低, 还有就是数组的大小是不可变的。
接下来介绍一种新的数据存储结构(链表)相对于数组来说还是有一些不同的地方。
链表:是一种链式的结构,相比数组,链表更加灵活,通过指针去进行增、删、改查,但是链表不能通过下标去查询。
一个链表结点包含数据域和指针域。
看看代码是如何实现: 在一个类中封装了数据,和指针,可以通过创建这个类,实现结点的创建。
private class Node{ //这个结点类属于链表的私有属性
E data; //数据域
Node next;//指针域
public Node() {
this(null , null);
}
public Node(E data , Node next) {
this.data = data;
this.next = next;
}
@Override
public String toString() {
return data.toString();
}
}
接下来我们看看什么是 “头结点” 和 “头指针”、尾指针。
头结点:是链表中的第一个结点
头结点分为:
- 真实头结点:简单说就是链表第一结点是直接存储数据的结点
- 虚拟头结点:其第一结点是数据域为空的链表
头指针:是存储头结点地址的指针。
尾指针:最后一个结点的指针。
知道了链表的特性后,我们来看一下单链表长啥样。
(单链表:由N个结点组成,且只有一个指针域。)
刚了解过头结点,那么用这两种方式来看看链表的结构:
可以很清楚的发现,头结点的不同。
看看类图思考如何去编写?(接下来的链表是以头尾指针同时存在的情况去实现的。)
代码编写LinedList类实现List接口:
public class LinkedList<E> implements List<E> {
-
单向链表的结点内部类
private class Node{ //这个结点类属于链表的私有属性 E data; //数据域 Node next; //指针域 public Node(){ this(null,null); } //构造函数 参数为数据元素和后继 public Node(E data,Node next){ this.data=data; this.next=next; } @Override public String toString() { return data.toString(); } }
-
链表的属性
private Node head; //指向虚拟头结点的头指针 private Node rear; //指向尾结点的尾指针 private int size; //记录元素的个数
-
-
public LinkedList(){ head=new Node(); //创建一个虚拟结点 rear=head; size=0; } //将数组转为链表 public LinkedList(E[] arr){ this(); for(E e:arr){ addLast(e); } } //获取有效元素个数 @Override public int getSize() { return size; } //判空 @Override public boolean isEmpty() { return size==0&&head.next==null; }
-
对于链表的插入需要分情况来处理,特殊情况头插法和尾插法,一般插入等。 在插入数据时 每次都把数据进行封装成结点在向链表中去添加。
-
说明 后继 = = 下一跳
-
头插法 1.步 将头指针的下一跳 给 新元素的下一跳 再将 新元素的 地址给 头结点的下 一跳 。如果链表为空的时候需要将尾结点指向新元素。
(对于单向链表 只知道后继 ,所以头插法进链表的顺序 1 ,2 ,3 那么出链表是 3 ,2 ,1 )和栈是一样的
-
尾插法 链表为空的时候 添加元素,将新元素的地址给 head的下一跳(在将尾指针后移),一般情况把把新元素的下一跳,给尾指针下一跳(尾指针后移)。
-
一般 将插入位置前的 下一跳 给 新元素的下一跳 再将新元素的地址给 插入位置前的下一跳。
@Override public void add(int index, E e) { if(index<0||index>size){ throw new IllegalArgumentException("插入角标非法!"); } Node n=new Node(e,null); if(index==0){ //头插 n.next=head.next; //将头指针的 下 一跳 给 新元素下一跳 head.next=n; if(size==0){ //特殊情况! rear=n; } }else if(index==size){ //尾插 rear.next=n; //将新元素的地址给 给 为结点 rear=rear.next; //移动尾指针 }else{ Node p=head; for(int i=0;i<index;i++){ p=p.next; } n.next=p.next; p.next=n; } size++; } @Override public void addFirst(E e) { add(0,e); } @Override public void addLast(E e) { add(size,e); }
-
获取元素数据,对链表而言没有下标的说法,所以必须要遍历元素,为了获取到元素,我们创建一个新的指针。从头指针开始,遍历知道需要获取的位置,通过获取他的结点中的数据。
@Override public E get(int index) { if(index<0||index>=size){ throw new IllegalArgumentException("查找角标非法!"); } if(index==0){ return head.next.data; }else if(index==size-1){ return rear.data; }else{ Node p=head; for(int i=0;i<=index;i++){ p=p.next; } return p.data; } } @Override public E getFirst() { return get(0); } @Override public E getLast() { return get(size-1); }
-
在获取的方法实现基础上将获取的位置进行修改数据即可。
@Override public void set(int index, E e) { if(index<0||index>=size){ throw new IllegalArgumentException("修改角标非法!"); } if(index==0){ head.next.data=e; }else if(index==size-1){ rear.data=e; }else{ Node p=head; for(int i=0;i<=index;i++){ p=p.next; } p.data=e; } } @Override public boolean contains(E e) { return find(e)!=-1; }
-
用链表的查找需要一直找下去,单向链表只能从前往后,如果找到就返回下标,没有找到就返回-1
@Override public int find(E e) { int index=-1; if(isEmpty()){ return index; } Node p=head; while(p.next!=null){ p=p.next; index++; if(p.data==e){ return index; } } return -1; }
-
因为链表没有下标,针对这个是单链表 所以找到删除点之前这个数才能删除这个数。
-
删头 将删除的元素的下一跳给头指针的下一跳 (删除的是最后一个元素,必须将尾指针拿回 头结点的地方)
-
删尾, 将尾指针指向 删除位置的前一个位置将删除位置前一个 的下一跳为空
-
一般删除 先找到删除位置前一个位置 ,将 删除元素的下一跳 ,给 删除这个位置前一个位置的下一跳.
@Override public E remove(int index) { if(index<0||index>=size){ throw new IllegalArgumentException("删除角标非法!"); } E res=null; if(index==0){ //头删 Node p=head.next; res=p.data; head.next=p.next; p.next=null; p=null; if(size==1){ rear=head; } }else if(index==size-1){//尾删 Node p=head; res=rear.data; while(p.next!=rear){ p=p.next; } p.next=null; rear=p; }else{ Node p=head; for(int i=0;i<index;i++){ p=p.next; } Node del=p.next; res=del.data; p.next=del.next; del.next=null; del=null; } size--; return res; } @Override public E removeFirst() { return remove(0); } @Override public E removeLast() { return remove(size-1); }
-
先找这个数的下标,然后判断是否有效 ,在执行删除
@Override public void removeElement(E e) { int index=find(e); if(index==-1){ throw new IllegalArgumentException("元素不存在"); } remove(index); }
-
清除
@Override public void clear() { head.next=null; rear=head; size=0; }
-
链表没有下标,只能通过前一个去找后一个,不断进行拼接打印出链表
@Override public String toString() { StringBuilder sb=new StringBuilder(); sb.append("LinkedList:size="+getSize()+"\n"); if(isEmpty()){ sb.append("[]"); }else{ sb.append("["); Node p=head; while(p.next!=null){ p=p.next; if(p==rear){ sb.append(p.data+"]"); }else{ sb.append(p.data+","); } } } return sb.toString();
-
比较方法对于链表中所有的数进行内容比较
@Override public boolean equals(Object obj) { if(obj != null) { return false; } if(obj == this) { return true; } if(obj instanceof LinkedList) { LinkedList l = (LinkedList)obj; if(getSize() == l.getSize()) { for (int i = 0; i < getSize(); i++) { if(get(i) != l.get(i)) { return false; } } return true; } } return false; } } }
二,单向循环链表
想想单向链表,那么循环的单向链表是怎样实现的呢?
定义是:将单链表的指针由空改为指向头结点的(或第一个元素节点),就使整个单链表形成一个环,这种有种头尾相接的单链表是单向循环链表。
简单来说:将单链表原尾指针指向空 改为 指向 链表的头节点,形成一个单行循环的环,这种结构的的链表是单向循环链表。
考虑头结点问题?看看有什么问题?
那么根据循环的特性偏向用真实头结点。还记得虚拟头结点链表,判空条件是 head = rear
,那么是真实头节点的情况,没有元素时,head = null && rear = null,则该链表为空。
对于单向链表有了一定的了解,那么我们可以试一试如何编写它。
public class LoopSingle<E> implements List<E> {
-
定义属性
private Node head; // 定义头指针 private Node rear; //定义尾指针 private int size; //元素个数
-
因为用真实头结点,多以创建刚开始无数据插入,就没有结点。
public LoopSingle() { head=null; rear=null; size=0; }
-
数组封装成链表
public LoopSingle(E[] arr){ } @Override public int getSize() { return size; } @Override public boolean isEmpty() { return size==0&&head==null&&rear==null; }
-
向线性表里加入元素。
-
1.首先靠率到,真实头结点 ,链表创建的时候,head = null,于 rear = null。
那么,在空链表下添加元素是不是属于特殊情况,那么对应的处理,就是让头指向这个结点,尾也指向这个结点,因为时循环链表那么需要将尾指针的下一跳指向头指针,构成一个环。
-
2.头插法:插入对像的下一跳等于头指针(插入前的head所指向的位置),新数据为头结点,让尾结点更新(永远指向头)。
-
-
尾插法,将添加进来的结点 的下一跳 等于 head (rear .next = head)因为插入进来永远是尾 所以 必须让尾指针的下一跳指向头指针
-
一般插入,先找到插入位置,用流动结点去获取到插入位置前一个结点,将新节点的下一跳 等于 找到流动结点的下一跳 ,将 p.next = n 将找到插入点之前的p的下一跳更新为 插入数的结点。
@Override public void add(int index, E e) { if(index<0||index>size){ throw new IllegalArgumentException("插入角标非法!"); } Node n=new Node(e,null); if(isEmpty()){ //特殊情况 head=n; rear=n; rear.next=head; }else if(index==0){ //头插 n.next=head; head=n; rear.next=head; }else if(index==size){//尾插 n.next=head; rear.next=n; rear=n; }else{ //一般情况 Node p=head; for(int i=0;i<index-1;i++){ p=p.next; } n.next=p.next; p.next=n; } size++; } @Override public void addFirst(E e) { add(0,e); } @Override public void addLast(E e) { add(size,e); }
-
找头、找尾是特殊情况,一般情况,通过一个游走的结点去获取需要的结点数据。
@Override public E get(int index) { if(index<0||index>=size){ throw new IllegalArgumentException("查找角标非法!"); } if(index==0){ return head.data; }else if(index==size-1){ return rear.data; }else{ Node p=head; for(int i=0;i<index;i++){ p=p.next; } return p.data; } } @Override public E getFirst() { return get(0); } @Override public E getLast() { return get(size-1); }
-
在获取数据的基础,去修改对应位置的数据。
@Override public void set(int index, E e) { if(index<0||index>=size){ throw new IllegalArgumentException("修改角标非法!"); } if(index==0){ head.data=e; }else if(index==size-1){ rear.data=e; }else{ Node p=head; for(int i=0;i<index;i++){ p=p.next; } p.data=e; } } @Override public boolean contains(E e) { return find(e)!=-1; } @Override public int find(E e) { if(isEmpty()){ return -1; } Node p=head; int index=0; while(p.data!=e){ p=p.next; index++; if(p==head){ return -1; } } return index; }
-
删除情况考率最后一个元素,反回头结点的数据,让头指针,和尾指针为空
-
删头 先获取头结点的数,让头指针等于 头指针的下一跳 ,尾指针的下一跳等于 更新后的head指针
-
删尾先获取值,然后找到尾指针前一个位置 用流动 结点 p 获取 ,让p.next = rear.next ,找到删除前的位置 让它的下一套为 rear 的下一跳(head),让尾指针指向 删除结点的前一个结点 p。
-
一般删除,先找一个遍历的流动结点p去找删除位置前一个位置,获取删除的结点(通过创建一个新结点(del 等于 p.next ) 将前一个位置的下一跳给 删除创建新的结点del),让p.next = del.next ,即将删除结点的下一跳(del.next)给删除结点前一个位置的下一跳(p.next),
@Override public E remove(int index) { if(index<0||index>=size){ throw new IllegalArgumentException("删除角标非法!"); } E res=null; if(size==1){ //特殊情况 res=head.data; head=null; rear=null; }else if(index==0){ res=head.data; head=head.next; rear.next=head; }else if(index==size-1){ res=rear.data; Node p=head; while(p.next!=rear){ p=p.next; } p.next=rear.next; rear=p; }else{ Node p=head; for(int i=0;i<index-1;i++){ p=p.next; } Node del=p.next; res=del.data; p.next=del.next; } size--; return res; } @Override public E removeFirst() { return remove(0); } @Override public E removeLast() { return remove(size-1); } @Override public void removeElement(E e) { remove(find(e)); } @Override public void clear() { head=null; rear=null; size=0; }
-
关键点在于如何将一个环走一圈
@Override public String toString() { StringBuilder sb=new StringBuilder(); sb.append("LoopSingle:size="+getSize()+"\n"); if(isEmpty()){ sb.append("[]"); }else{ sb.append('['); Node p=head; while(true){ sb.append(p.data); if(p.next==head){ sb.append(']'); break; }else{ sb.append(','); } p=p.next; } } return sb.toString(); }
-
内置节点(链表特性)
private class Node{ E data; //数据域 Node next; //指针域 public Node(){ this(null,null); } public Node(E data,Node next){ this.data=data; this.next=next; } @Override public String toString() { return data.toString(); } } }