第二章 线性表之单链表(二)
1、链表
定义: 链表是一种逻辑结构为线性表的格式,而它在物理存储单元上是非连续、非顺序的存储结构。他的存储的元素单元由一个链表节点所构成,这个节点包括对应的存储数据和指向下一个节点的指针,链表之间的连接也就是靠节点中的指针来进行关联。它和顺序表的差别就是存储结构的不同。
链表节点:
在链表中,每一个数据元素包括两个部分,一个部分是数据部分(data),另一个部分是指向下一个节点的指针(next),链表通过一个数据节点中的next指针,指向下一个节点来达到将所有的数据节点连接成一张链表,且next指针其实就是存储着下一个节点的地址。所以链表的一个存储的结构如下图所示:
链表结构:
链表结构需要先声明一个空节点,这是为了以后我们遍历方便,所以链表是从一个空节点开始的,通过空节点的next指针指向下一个元素节点,以此类推连接成一张列表,正因为链表的这种结构,所以就使得对应链表来说,要访问一个元素节点,就得从空节点开始,通过每一个节点的next指针指向下一个节点来获得对应的元素节点的访问,也就是我们所有的对于元素的遍历,都得从空节点开始,然后通过每一个节点得next指针来获得下一个数据元素,通过这种方式来获取到对应得节点。
2单链表
2.1单链表的基本实现
链表中的每一个数据元素是由一个数据和指向下一个节点的指针所组成,所以对于链表来说,它的一个数据元素是要设计一个节点类来确定的,所以在实现 链表之前,我们需要先去设计且实现一下链表中的节点。
class SingleNode<E>{
E item;//存储需要操作的数据
SingleNode<E> next;//指向下一个SingleNode节点的指针
public SingleNode(E item) {
this.item = item;
}
public SingleNode(E item, SingleNode<E> next) {
this.item = item;
this.next = next;
}
public SingleNode() {
}
public E getItem() {
return item;
}
public void setItem(E item) {
this.item = item;
}
public SingleNode<E> getNext() {
return next;
}
public void setNext(SingleNode<E> next) {
this.next = next;
}
}
以上代码就是节点类的设计以及实现,它是由一个数据item和指向下一个节点的指针next组成,这样就使得链表中的一个节点可以存储数据item,并且通过指针next去指向下一个节点,来实现一个链表。
实现完链表的节点后,我们就可以去设计以及实现链表了。对应的链表的API设计为实现List接口,实现接口里面的所有方法即可,他没有自己的成员方法,只有实现接口的成员方法,下表是对应的SingleLinkedList的API的设计。
List接口:
public interface List<E> extends Iterable<E>{
int size();
boolean isEmpty();
boolean contains(E element);
void clear();
E get(int index);
void add(int index, E element);
void add(E element);
E remove(int index);
boolean remove(E element);
int indexOf(E element);
E set(int index, E element);
Iterator<E> iterator();
}
类名 | SingleLinkedList |
---|---|
构造方法 | SingleLinkedList(int initialCapacity) :创建容量链表,也就是生成一个空节点,空节点的next指针为空 |
成员变量 | private SingleNode head;//该单链表的空头节点 private int size;//记录该单链表的长度 |
实现的 成员方法 | 1.int size(): 返回线性表的长度 2.boolean isEmpty():判断线性表是否为空 3.boolean contains(E element):判断线性表中是否包含有传入的数据元素 4.void clear():清空该线性表 5.E get(int index):获得该线性表对应位置的元素 6.void add(int index, E element):将元素element插入到序号为index的位置 7.boolean add(E element):向线性表的尾部添加元素element 8.E remove(int index):移除序号为index位置的元素 9.boolean remove(E element):移除线性表中元素值为element的元素 10.int indexOf(E element):获得线性表中值为element元素的序号 11.E set(int index, E element):更新序号为index的元素,设置成传入的element |
自己的 成员方法 |
具体实现:
package LinearList;
import java.util.LinkedList;
public class SingleLinkedList<E> implements List<E>{
private SingleNode<E> head;//该单链表的空头节点
private int size;//记录该单链表的长度
public SingleLinkedList() {
clear();//置空操作就是在重新生成一个空链表
}
@Override
public int size() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean contains(E element) {
SingleNode p = head;
while(p.next != null){//p.next表示的是判断当前节点是否有后驱节点
if(p.next.item.equals(element)){//如果当前节点的后驱节点的数值部分等于传入的参数,则就跳出循环
return true;
}
p = p.next;
}
return false;
}
@Override
public void clear() {
head = new SingleNode<>();//空头节点置空
size = 0;
}
@Override
public E get(int index) {
if(index < 0 || index >= size){
return null;
}
SingleNode<E> p = head;
for (int i = 0; i < index; i++) {
p = p.next;
}//将p指针移动到第index节点的前一个节点,即指向index-1号节点
SingleNode<E> indexNode = p.next;//获取到第index的节点
return indexNode.item;
}
@Override
public void add(int index, E element) {
if(index < 0 || index > size){
return;
}//index不越界
SingleNode p = head;
for (int i = 0; i < index; i++) {
p = p.next;
}//将p指针移动到第index节点的前一个节点,即指向index-1号节点
SingleNode newNode = new SingleNode(element);
newNode.next = p.next;//将新节点的next指针指向index号节点
p.next = newNode;//将index-1号节点的next指针指向新节点
size++;
}
@Override
public void add(E element) {
this.add(size,element);
}
@Override
public E remove(int index) {
if(index < 0 || index > size){
return null;
}//index不越界
SingleNode p = head;
for (int i = 0; i < index; i++) {
p = p.next;
}//将p指针移动到第index节点的前一个节点,即指向index-1号节点
SingleNode<E> indexNode = p.next;
E removedE = indexNode.item;
p.next = indexNode.next;//把第index-1号节点的next指针指向第index+1号节点
size--;
return removedE;
}
@Override
public boolean remove(E element) {
SingleNode p = head;
while(p.next != null){//p.next表示的是判断当前节点是否有后驱节点
if(p.next.item.equals(element)){//如果当前节点的后驱节点的数值部分等于传入的参数,则就跳出循环
break;
}
p = p.next;
}
if(p.next == null){//如果p.next==null就证明指针p遍历到了最后一个节点都没有找到元素element
return false;
}
SingleNode<E> removedNode = p.next;
p.next = removedNode.next;
size--;
return true;
}
@Override
public int indexOf(E element) {
SingleNode p = head;
int i = 0;//记录遍历指针p的位置
while(p.next != null){//p.next表示的是判断当前节点是否有后驱节点
if(p.next.item.equals(element)){//如果当前节点的后驱节点的数值部分等于传入的参数,则就跳出循环
break;
}
p = p.next;
i++;
}
if(p.next == null){//如果p.next==null就证明指针p遍历到了最后一个节点都没有找到元素element
return -1;
}
return i;
}
@Override
public E set(int index, E element) {
if(index < 0 || index > size){
return null;
}//index不越界
SingleNode p = head;
for (int i = 0; i < index; i++) {
p = p.next;
}//将p指针移动到第index节点的前一个节点,即指向index-1号节点
SingleNode<E> indexNode = p.next;//得到第index号节点的引用
E oldEle = indexNode.item;
indexNode.setItem(element);
return oldEle;
}
private class SingleNode<E>{
E item;//存储需要操作的数据
SingleNode<E> next;//指向下一个SingleNode节点的指针
public SingleNode(E item) {
this.item = item;
}
public SingleNode(E item, SingleNode<E> next) {
this.item = item;
this.next = next;
}
public SingleNode() {
}
public E getItem() {
return item;
}
public void setItem(E item) {
this.item = item;
}
public SingleNode<E> getNext() {
return next;
}
public void setNext(SingleNode<E> next) {
this.next = next;
}
}
}
对于链表的函数的实现,需要明确的如下几点:
1、空链表中只有一个空头节点(head),这个节点数据item为null,且next也为空。若链表不为空,则空头节点的next指针就指向链表的第一个节点。
2、我们要在链表中找到某一个节点,就必须从链表的头结点开始找起,然后通过不断的循环的去得到节点的next指针来去寻找对应的节点。
3、在我们实现链表的过程中,都是去声明一个记录指针p,然后通过不断的遍历,将指针p移动到要增加节点或者要移除的节点位置。
在我们链表的实现过程中,涉及到的添加和删除操作等都需要我们去遍历链表,我们的遍历方式是去声明一个指针p,先让p指向头节点,然后再通过p = p.next去不断的循环,让p移动到需要操作的位置。如我们需要在index=3处添加一个节点,我们的指针p的移动过程如下:
我们通过将指针p指向了index=2的位置,这样,我们就可以通过p.next得到index=3出的索引了,所以无论我们删除还是添加一个节点,都是将指针p移动到需要操作的位置的前一个节点的位置,这样我们才能够删除或者添加对应节点,并把链表连接起来。
我们通过前面的操作,将p指针指向了index=2的位置,现在我们需要将新的节点插入到2和3之间,具体的操作过程如下:
第一步:通过代码nexNode.next = p.next,让新的节点的next指针指向节点3
第二步:通过代码p.next = newNode,将index=2的节点的next指针指向新的节点
通过这两步,我们就实现的新节点的插入。
2.2 单链表的遍历
对于该链表得遍历,我们可以直接通过for循环就能实现遍历,但是如果我们想使用java中提供得增强for循环foreach来进行遍历得话很显然不行,因为使用增强for循环必须得实现对应得遍历器才可以。所以我们去实现顺序表得遍历器来达到使用foreach来进行遍历,实现遍历器的步骤为:
1. 必须实现Iterable 接口,并且实现该接口中的public Iterator iterator(){}方法,该方法中需要返回一个如何去进行遍历的遍历对象。
2. 去实现一个Iterator接口的内部类SLIterator,并且实现该接口中的hasNext()、next()方法,这两个方法就确定了如何去遍历对应的顺序表。
3.在步骤1中的public Iterator iterator(){}方法中返回步骤2中的内部类SLIterator的对象。
package LinearList;
import java.util.Iterator;
import java.util.LinkedList;
public class SingleLinkedList<E> implements List<E>,Iterable<E>{
private SingleNode<E> head;//该单链表的空头节点
private int size;//记录该单链表的长度
...
与上面代码一致。
....
@Override
public Iterator<E> iterator() {
return new SLIterator();
}
private class SLIterator implements Iterator<E>{
private SingleNode<E> pNode;
public SLIterator() {
this.pNode = head;//指向链表的头节点
}
@Override
public boolean hasNext() {
return pNode.next != null;//如果当前节点next指针不为空,则表示还有下一个节点
}
@Override
public E next() {
pNode = pNode.next;//移动到下一个节点
return pNode.item;//返回节点的值
}
}
}
2.3 单链表的时间复杂度
方法 | 时间复杂度分析 |
---|---|
E get(int index) | 单链表是通过节点中的next指针去将节点连接起来形成一个链表,所以要找到该链表中的某一个节点, 就得去找到该节点的前驱 节点,所以需要从头节点开始去一个一个节点的去遍历寻找。 在最坏的情况下链表要遍历n此才能获取到需要的节点,所以时间复杂度为O(n). |
public void add(int index, E element) | 我们在单链表表中插入一个元素时,需要去遍历我们得链表,去找到我们需要插入的节点位置的前一个 节点的位置,当找到这个位置后,只需要去改变next指针的地址就行,不需要移动节点,所以主要花费的时间是在找节点的位置出。 所以它的时间复杂度和查找节点值一样,因此该方法的时间复杂度为O(n). |
public E remove(int index) | 和节点的添加的复杂度一样,也是需要找到需要删除节点的前一个位置,所以时间复杂度为O(n). |
在单链表中,对于节点的删除和添加都是比较简单,就是将节点的next指针的值改变一下就可以实现,但是前提之下我们得需要去找到我们要删除或者要添加得位置,也就是我们得要去遍历我们得链表,找到我们要操作得位置,所以基本得时间都是花费在遍历节点上。