数据结构——链式存储结构(单向链表)

关于顺序存储结构的内容,就止步于双端队列了。接下来要介绍的是链表的内容,相对用于实现顺序表的数组来说,链表的定义即实现就比较抽象和麻烦一些,而且还难!but!虽然难,“我”也不会气馁!该说还得说啊,虽然自己是个菜鸟,但是总有一天,......算了,先介绍一下链表是啥吧。

先来看一条生活中常见的链子:

我们发现,生活中不管金链子也好、还是银链子也好、亦或者是铁的都是由一个小小的连在一起,然后组成了一条链子。也就是说:

                                                        

所以运用生活中的原理,我们就创建出了链表这一概念。

动态链表

为了表示每个数据元素ai与其直接后继数据元素ai+1 之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称做指针或链。这两部分信息组成数据元素ai的存储映像,称为结点(Node)。【这是官方定义!】

结点

简单点来说吧!这个结点就可以看成上面图片中的一个环,然后然后环环相连,组成一条的链子就是链表,与实际的链子不同的是,实际的链子中的环是空的,但是它套着下一个环,而在数据结构中,这个结点赋予了特殊含义,首先它是一个类,然后这个类里面有两个属性:一个是数据域,存储我们需要存储的数据;另一个是指针域,存储的是下一个结点所在的地址,方便我们找到该结点的位置。就是因为有这个指针域将下一个结点的地址存储,才形成了逻辑上的链式存储。结点以及链表大概长这个样子:

                  

因此,n个结点(ai 的存储映像)链结成一个链表,即为线性表(a1, a2, ....,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域, 所以叫做单链表

头指针和尾指针

那么问题来了。我们知道每个结点的指针域存储的是下一个结点所在的位置,这个位置是内存随机分配的,我们并不知道。因此,第一个结点的位置我们是不知道的,那么这个链表该从哪儿开始查找呢?为了解决这个问题,需要设置一个头指针。

我们把链表当中的第一个结点叫做头结点,在链式存储的结构当中,头结点可虚可实,什么意思呢?就是分为真实的头结点——它存储元素,也存储下一个结点的地址,是真真正正的老大!虚拟的头结点——不存储元素,只存储下一个结点的地址,他不是真真的老大,真正的老大是它的下一个,但是虚拟头结点是一个真真切切的结点!表现形式如下:

 

因此,为了拿住这个链表,擒贼先擒王,首先要获得这个链表的头结点的位置,所以,需要一个指针并且我们可以访问的到,来存储头结点的地址——这个就是头指针,而这个指针仅仅是一个引用变量而已,只用来存储头结点的地址

当然做事要有始有终,既然有头指针,相应的就应该有尾指针,表示该链表最后一个结点的位置存储的是最后一个结点的地址,以便遍历的时候,判断是否遍历到最后一个位置。当然我们在链表结构中也会设置一个size表示长度,然而现在是链式存储的结构,我们应该用链式的特性去操作链表,不推荐用size。当然设置尾指针还有一个好处就是,因为要查找到某个结点时,必须要查找到其前一个结点,每次都要从头遍历,如果要实现尾插法的话,每次尾插就要遍历一遍,此时有尾指针就可直接获得最后一个元素的地址,把当前尾结点的指针域指向新进来结点的地址,然后尾指针后移就可。省去了遍历的时间复杂度

线性表

至此有关动态链表的内容就介绍完了,然后就是具体的实现。在顺序存储结构中有线性表,是用数组的实现的,现在已经知道了链表的内容,这里我们可以用链表实现一个线性表。具体方法如类图,具体方法的实现如代码:

链表实现的线性表仍然是继承List接口,其实在Java内置工具中,List接口下面也有一个LinkedList子类,就是拿链表实现的。链表是由结点组成的,因此LinkedList类里面包含一个Node类,在之前介绍链栈和内部内的时候说过这个结构。两个类之前的关系应该是组合关系,因为链表没了结点就不能是链表了。但又是部分与整体的关系,所有这里是组合或者聚合关系都对。

然后我们再来看看用链表怎么实现线性表的函数操作(本次实现的线性表采用的是有虚拟头结点的结构实现):

package 线性表; //目前我们所写的所有代码包括实现的接口都是我们自己写的

public class LinkedList<E> implements List<E>{

	private class Node{  //首先定义一个结点类,这个类是是组成链表的关键,
    //相当于也是链表的一个属性,因此要定义成一个私有的,外界就不能创建结点改变链表内部的结构了
		E data;  //数据域,因为在结点的外部,我们在给结点的数据域存放数据之后,还要能访问到,因此是共有的
		Node next; //指针域,同数据域
		public Node() { //创建Node不带参的构造函数
			this(null,null);
		}
		public Node(E data,Node next) {  //创建Node代参的构造函数
			this.data=data;
			this.next=null;
		}

		@Override
		public String toString() {  //重写Node的toString方法,方便在打印出结果时,
                                        //是我们想要看到的数据,而不是地址或其他
			return data.toString();
		}
	}
	
	private Node head; //指向头结点的头指针 
	private Node rear;  //指向尾结点的尾指针
	private int size;  //记录元素个数
	
	public LinkedList() { //LinkedList的构造函数,初始状态是有一个虚拟的头结点,
                                //头尾指针都指向这个虚拟的头结点,有效元素为0个
		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 isEmputy() {//判空,查看是否是在初始化的状态,切记虚拟头结点不算一个元素
		return size==0&&head.next==null;
	}

   然后我们看看链表怎么进行插入:

在头插:

在尾插:

在中间插:

 

因此代码实现一个指定角标插入元素:

@Override
	public void add(int index, E e) {
		if(index<0 || index>size) {  //先判断添加的位置是否合法
			throw new IllegalArgumentException("插入角标非法!");
		}
		Node n = new Node(); //如果合法,肯定会有一个新的结点创建
		n.data=e; //新结点的数据域存储的是要插入的元素
		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++; //只要添加了,最后的有效元素都增加1喔
	}

	@Override
	public void addFirst(E e) { //头插
		add(0,e); //调用方法,传0
	}

	@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); //传入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表示不存在,不为-1,表示存在
	}

	@Override
	public int find(E e) { //查找某元素的下标
		int index=-1;//首先表示不存在
		if(isEmputy()) { //如果链表为空
			return -1; //肯定不存在
		}else { //不为空,就要遍历查找
			Node p=head; //从虚拟头结点开始
			while(p.next!=null) { //只要下一个结点不为空
				p=p.next;将真正的头取代p
				index++;表示的位置在增加
				if(p.data==e) { //如果此结点存储的数据与我们要找的数据相等
					return index; //将此结点的位置返回
				}//否则继续向下找
			}
		}
		return -1; //当循环正常结束也是没中断返回,那就是不存在,返回-1
	}

如何删除一个结点:

删头:

删尾:

 

删除中间结点:

 

代码实现:

@Override
	public E remove(int index) { //指定角标删除结点
		if(index<0 || index>size) {  //判断指定角标是否在合法范围内
			throw new IllegalArgumentException("删除角标非法!");
		}
		if(index==0) { //删头
			Node p=head.next; //先将头结点提出来
			E e = p.data; //将头结点的数据取出来
			head.next=p.next; //将头结点的下个结点的地址存到虚拟头结点的指针域里
			p.next=null; //将原头结点指针域置空与原链表断开关系
			p.data=null; //数据域清空
			if(size==1) { //此时特殊在删除的是最后一个结点
				rear=head; //那么尾指针要指向虚拟头结点的位置
			}
			size--; //删除后有效元素减少1
			return e; //将已经删除的元素返回
		}else if(index==size-1) { //删尾
			Node p=head; //从头遍历
			E e = rear.data; //将尾指针的数据取出来
			while(p.next!=rear) { //遍历到尾结点的前一个结点
				p=p.next;
			}
			p.next=null; //将尾结点前一个结点中存的尾结点的地址清空
			rear=p; //此时尾指针指向尾结点的前一个结点的位置
			size--; 
			return e;
		}else { //删中间
			Node p=head; //从头遍历
			for(int i=0;i<index;i++) {
				p=p.next;
			} //遍历到要删位置的前一个结点
			Node d = p.next; //将要删除的结点取出来
			E e= d.data; //再将要删除结点的数据取出来
			p.next=d.next; //将要删除结点的下一个结点的地址存到要删结点的前一个结点的指针域
			d.next=null; //此结点指针域置空
			d.data=null; //数据域置空
			size--;
			return e;
		}
	}

	@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(isEmputy()) {
			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(this==obj) { //传进来的是自己
			return true;  //一定相等
		} 
		if(obj instanceof LinkedList) { //传进来的对象是本类
			LinkedList<E> list1 = (LinkedList<E>) obj; //强转一下保险
			if(this.getSize()==list1.getSize()) { //如果两个链表长度相同
                                //就从两个链表的头开始遍历,依次比较内容是否相等
				Node p1 = this.head.next; 
				Node p2 = list1.head.next;
			while(p1!=null&&p2!=null) {
				if(p1.data!=p2.data) { //只要有一个数据不相等
					return false; //就不相等
				}else {
					p1=p1.next;
					p2=p2.next;
				}
			}
			return true;
			}
			return false;
		}
		return false;
	}
}

好了,关于链表的操作基本都实现了,链表的好处就在于不用扩容,增删快,但是查找起来非常慢,但是不会浪费多余的空间。希望这篇文章对你有所帮助,不懂的随时滴我喔!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章