单链表
数据在计算机中的存储形式有两种,顺序存储结构是把数据元素存放在地址连续的存储单元里,我们前面写的线性表、顺序栈、循环队列都是顺序存储结构,基于动态数组实现的,这个结构是占用连续的存储单元,比较浪费空间,把数据元素存放在任意的存储单元里,就可以充分的利用空间,这种存储结构就是链式存储结构。
链式存储结构并不能反映数据元素之间的逻辑关系,因此需要 用一个指针存放数据元素的地址,这样地址就可以通过相关联数据元素的位置,这样链式存储就灵活很多了,数据存在哪里不重要,只要有一个指针存放对应的地址就能找到它了。
为了表示每个数据元素a与其直接后继数据元素b之间的逻辑关系,对数据元素a来说,除了存储其本身的信息之外,还需存储一个指示其后继的信息。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素a的存储映象,称为结点(Node)。
n个结点(a的存储映象)链结成一个链表,即线性表的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
头结点和头指针
头结点是指链表中的第一个结点,有真实头结点和虚拟头结点之分
真实头结点:其第一个结点用于存储数据
虚拟头结点:其第一个结点不许存储数据
顺便说一下,头指针和尾指针都仅仅是一个引用变量,分别是链表头结点和最后一个结点的指针而言。
其实我们发现链表和顺序表和基本操作差不多,只是把元素放入了结点中而言,所以对于单链表还是可以让它实现List接口,重写接口中的抽象方法即可。
下面用Java语言实现线性表链式存储结构,采用虚拟头结点方式,链式存储结构需要结点这个东西,把它单独放在一个类,作为内部类,类中定义结点的两个部分,数据域data和指针域next。链表的一些基本操作都是基于结点实现。
package DS02.动态链表;
import DS01.动态数组.List;
import java.util.Iterator;
//用动态链表实现线性表 链表
//头插法:像栈
//尾插法:像队列
//头插尾插结合
public class LinkedList<E> implements List<E> {
private Node head; //链表的头指针
private Node rear; //链表的尾指针
private int size; //链表的元素个数(结点个数)
//构造函数
public LinkedList(){
head=new Node();
//刚开始给一个空的结点
rear=head; //头尾结点一样,即空表 再加结点头指针不动尾指针动就ok
}
//内部类
class Node{ //链表内部的东西,内部类私有,外部不需要知道结点的存在
E data; //数据域 类型由外界决定
Node next; //指针域
//构造函数
Node(){
this(null,null);
}
Node(E data,Node next){
this.data=data;
this.next=next;
}
@Override
public String toString() {
return data.toString(); //由调用者决定,结点的toString
}
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size==0&&head==rear;
}
}
这里比较重要的是,单链表中插入和删除元素,先对插入元素进行分析,有头插法,尾插法,头插尾插结合还有一般插入,分别分析其过程:
- 头插法: 先把虚拟头结点的指针域存放的物理地址给新结点的指针域,头结点的指针域指向新结点的地址,就完成了表头插入元素的操作
- 尾插法:先把新结点的物理地址给尾结点的指针域,新结点的地址给尾指针
- 头插尾插结合:头插和尾插在链表为空时,即插入第一个元素时实现方法是一样的,后面的元素进入就分头插尾插两方面
- 一半插入:借助指针p遍历找要插入的位置,把p指针指向结点下一结点的地址给新结点的指针域,再将新结点的地址给p结点的指针域
代码实现如下:(代码中没有头尾结合插入的代码)
//插入元素
@Override
public void add(int index, E e) {
if(index<0||index>size){ //角标越界
throw new IllegalArgumentException("角标越界");
}
//创建新的结点
Node n=new Node();
n.data=e; //结点的数据域存e
if(isEmpty()){ //空表状态 特殊处理
head.next=n;
rear=n;
}else if(index==0){ //头插法
n.next=head.next;
head.next=n;
}else if(index==size){ //尾插法
rear.next=n;
rear=n;
}else{ //中间插入
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
n.next=p.next; //把p指针指向结点下一结点的地址给新结点的指针域
p.next=n; //新结点的地址给p结点的指针域
}
size++; //有效元素+1
}
@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(isEmpty()){
throw new IllegalArgumentException("空表");
}
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; //借助指针p找到index对应结点
for(int i=0;i<=index;i++){
p=p.next;
}
return p.data; //返回指针p处结点数据域
}
}
@Override
public E getFirst() {
return get(0);
}
@Override
public E getLast() {
return get(size-1);
}
修改元素和查看是否包含该元素方法的前提是找到元素,借助指针p遍历链表查找角标index对应元素,实现代码如下:
@Override
public void set(int index, E e) {
if(isEmpty()){
throw new IllegalArgumentException("空表");
}
if(index<0||index>size){
throw new IllegalArgumentException("角标越界");
}
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) {
Node p=head;
for(int i=0;i<=size;i++){
p=p.next;
if(p.data.equals(e)){ //p指针结点处存放的元素与e比较
return i; //返回角标
}
}
return -1;
}
删除元素和插入元素一样分头删、尾删和中间删,其实三种方式的删除方法一样,都是断了要删除结点与其他结点的联系,让其被回收,下面具体讨论:
-
头删:创建变量del存放要删除的结点,也就是头指针处结点,把要删除元素结点的元素给变量ret,用于返回。让头结点指针域指向删除元素的下一个,再将del的指针域指向空,断了del和其他结点的联系,del会被回收器回收
-
尾删:要删除元素,要先找到要删除元素的前一个,将它的指针域指向空,将尾结点指针域指向空,尾结点会被回收器回收
-
中间删:先将要删除的元素用变量del存放,中间删除也是找要删除结点的前一个,借助指针p遍历寻找,将它的指针域指向del的下一个结点地址,即跳过del,再将del的指针域指向空,del被回收
还有一种特殊情况,就是链表内只有一个元素的情况,把它单独考虑一下,减小时间复杂度,在执行删除后记得让size-1
//删除结点
@Override
public E remove(int index) {
if(isEmpty()){
throw new IllegalArgumentException("空表");
}
if(index<0||index>=size){
throw new IllegalArgumentException("角标越界");
}
E ret=null;
if(size==1){ //只有一个结点的情况
ret=rear.data;
head.next=null;
rear=head;
}else if(index==0){ //要删除元素在表头
Node del=head.next;
ret=del.data;
head.next=del.next;
del.next=null;
del=null;
}else if(index==size-1){ //要删除元素在表尾
Node p=head;
while(true){
if(p.next!=rear){
p=p.next;
}else{
break;
}
}
ret=p.next.data;
p.next=null;
rear=p;
}else{ //要删除元素在中间的情况
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
Node del=p.next;
ret=del.data;
p.next=del.next;
del.next=null;
del=null;
}
size--; //有效元素-1
return ret; //返回被删除的元素
}
@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){
remove(index);
}else{
throw new IllegalArgumentException("找不到");
}
}
清空链表和以字符串形式打印,方便测试方法的正确性
@Override
public void clear() { //清空表
head.next=null;
rear=head;
size=0;
}
//按数组的格式打印
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("LinkedList: "+size+"\n");
sb.append('[');
if(isEmpty()){
sb.append(']');
}else{
Node p=head;
while(true){
if(p.next!=rear){
p=p.next;
sb.append(p.data);
sb.append(',');
}else{
sb.append(rear.data);
sb.append(']');
break;
}
}
}
return sb.toString();
}
迭代器,写内部类,创建对象LinkedListIterator,目的是可以循环输出链表内的元素,并支持foreach循环。
//迭代器
@Override
public Iterator<E> iterator() {
return new LinkedListIterator();
}
//内部类
public class LinkedListIterator implements Iterator<E>{
private Node p=head;
@Override
public boolean hasNext() {
return p.next!=null;
}
@Override
public E next() {
p=p.next;
return p.data;
}
}
当然的,每写一个方法,都在测试类中做个测试,有错误及时改正,养成一个良好的写代码习惯。