线性表的链式存储结构

前面学习过线性表的顺序存储结构。但它是有缺点的,最大的缺点就是插入和删除时需要移动大量元素。为了解决这个缺点,我们可以这样想。我们不再考虑元素的相邻位置,只让每个元素知道下一个元素的位置就可以合理的解决这个问题了。

于是,我们有了链表这个概念。
在这里插入图片描述
链表的节点分为两部分,为数据域和指针域。数据域存放数据元素信息,指针域存放其后继指针地址(即后继节点的存储位置)。

我认为链表就好比是玩具的小火车一样,有一个火车头,有很多车厢。每节车厢前后都有挂钩的地方,用来连接前面和后面的车厢,而火车头只有后面的挂钩。当我们需要拿掉哪一节车厢或者在某处加入一个车厢时,只需要把它的挂钩取下或者加入新车厢挂到挂钩上。
这里的火车头就指的是链表的头结点,而每节车厢就相当于链表中的节点。挂钩就当于链表中的指针。

在这里插入图片描述
我们在学习中采用虚拟头结点。
接下来,我们要用代码实现这些功能。
先来看一下线性表的链式存储结构的定义
在这里插入图片描述
先用LinkedList实现之前写的List这个接口,链表中存在节点,所以要再写一个节点这个类(即Node类),Node里面有数据域和指针域,这里Node算是LinkedList里面的内部类。
1.链表的类的定义LinkedList

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	//返回传入对象的数据类型的tostring方法
		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);
		}
	}

下面我们来看一下链表的插入和删除。
因为我们不确定插入的位置和链表中节点的个数,所以我们将插入情况分为三种。
一.头插法
1.链表为空时头插法

在这里插入图片描述
head为头指针指向头结点,rear为尾指针指向尾节点。
此时是链表为空的情况,头指针和尾指针都指向虚拟头结点,而虚拟头结点的指针则指向空,因为此时链表为空。
在这里插入图片描述
此时我们要将“A”元素插入到链表中,首先要将“A”元素封装到一个节点中。此时这个新节点的指针指向空,因为它后面没有其他节点。
准备插入时,1.我们把虚拟头结点的后继指针赋给新节点的后继指针,让新节点指向空,2.然后让虚拟头结点指向新节点。3.再让尾指针rear指向新节点。这样,我们的插入就完成了。
因为链表为空,当我们插入新节点时,新节点了就成了尾节点,所以要移动rear指针。
链表为空时的头插法相当于一种特殊的尾插法。
效果如下图。
在这里插入图片描述
2.链表不为空时头插法
在这里插入图片描述
此时我们要把“B”元素所在的新节点用头插法插入到链表中,也就是把“B”元素所在的节点插入到“A”元素所在节点的前面。

1.我们把虚拟头结点的指针后继指针赋给“B”元素所在的节点,等于就让“B”元素所在的节点指向了“A”元素所在的节点,2.然后再让虚拟头结点指向“B”元素所在的节点。这样,我们的头插法就完成了。
效果如下图

在这里插入图片描述
此时的尾指针Rear不用变化,因为是头插法,所以链表的尾节点是不会变动的。

二.尾插法
1.链表为空时尾插法
链表为空时,没有真实的头节点和尾节点,所以插法的方法和链表为空时头插法是一样的。
2.链表不为空时尾插法
在这里插入图片描述
此时我们要把元素“B”所在的节点通过尾插法插入到链表中。
先封装“B”元素到节点中,这不用多说。
1.我们让插入前的尾节点指向新节点(因为尾节点都指向空)
2.再让rear指针指向插入新节点后的尾节点,也就是“B”元素所在的节点
插入完成,效果如下图

在这里插入图片描述
三.头插尾插结合

在这里插入图片描述
头插尾插结合就是把上面的头插法和尾插法结合起来运用。
1.先把虚拟头结点的后继指针赋给“A”元素所在的节点,让虚拟头结点指向“A”元素所在的节点。
2.让rear指针指向“A”元素所在的节点
3.把虚拟头结点的后继指针赋给“B”元素所在的节点,等于此时“B”元素所在的节点指向了“A”元素所在的节点。
4.让虚拟头结点指向“B”元素所在的节点

四.一般插入
在这里插入图片描述
1.把“A”元素所在节点的后继指针赋给“D”元素所在的节点
2.让“A”元素所在节点指向“D”元素所在节点
效果如下图

在这里插入图片描述
2.插入代码如下:

@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);
	}

五.删除头部元素
1.一般删头
在这里插入图片描述
1.把“A”元素所在节点的后继指针赋给虚拟头结点,相当于虚拟头结点指向“B”元素所在的节点
2.让“A”元素所在节点指向null
效果如下图
在这里插入图片描述
2.只有一个节点时删头

在这里插入图片描述
1.把“A”元素所在节点的后继指针赋给虚拟头结点
2.让“A”元素所在节点指向null
2.让rear指针指向虚拟头结点

六.删除尾部元素

在这里插入图片描述
此时有三个节点,我们要删除尾节点,也就是“C”元素所在节点
1.先从头开始找,找到要删除尾节点的前一个节点
2.让rear指针指向要删除节点的前一个节点
3.让rear指向的节点(即“B”元素所在节点)指向null
效果如下图
在这里插入图片描述
七.一般删除
在这里插入图片描述
此时我们要删除“B”元素所在节点
1.把“B”元素所在节点的后继指针赋给“A”元素所在节点,即现在“A”元素所在节点指向了“C”元素所在节点
2.让“B”元素所在节点指向null
效果如下图
在这里插入图片描述
3.删除代码如下:

@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);
	}

4.getSize()
获取元素个数

@Override
	public int getSize() {
		return size;
	}

5.isEmpty()

@Override
	public boolean isEmpty() {
		return size==0&&head.next==null;
	}

6.给定下标返回查找元素 get()
先判断下标是否非法
下标为0时返回(真实头结点)的数据
下标为size-1时返回最后一个节点的数据
其他情况就遍历数据然后获取指定下标的数据

@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);
	}

7.给定下标和元素修改数据 set()
修改和获取很像,都是先判断下标是否非法
然后分下标为0时和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;
		}
	}

8.查找 find()
这里我们让下标从-1开始
先判断是否为空
我们要查找所以肯定从第一个节点开始遍历
我们不知道要循环的次数,但是知道终止循环的条件。所以我们用while循环,当p指针指向最后一个节点时,终止循环。

@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;
	}

9.判断是否包含 contains()
因为前面已经写过find函数,这里只需要调用find函数即可。
包含就返回节点的下标,不包含就返回-1.

@Override
	public boolean contains(E e) {
		
		return find(e)!=-1;
	}

10.清空链表 clear()
清空列表等于就只剩下虚拟头结点了
让head和rear都指向虚拟头节点即可
再让size=0

@Override
	public void clear() {
		head.next=null;
		rear=head;
		size=0;
	}

11.toString()
拼接字符串,返回链表的元素和长度

@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();
	}

12.equals()比较函数
先判断传入数据类型是否属于链表
是的话先判断元素个数是否相等
相等再遍历元素判断是否相等
最后返回结果

@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;
	}

代码部分就到这里,后面需要再写一个main函数进行测试。

线性表的链式存储结构就到这里,随着不断的学习,我会进行补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值