数据结构(四)--------单向链表、单向循环链表

动态链表:

在学过数组后,发现数组的存储结构有一定的缺陷。就是增(插入位置)、删、改效率低, 还有就是数组的大小是不可变的。
接下来介绍一种新的数据存储结构(链表)相对于数组来说还是有一些不同的地方。

链表:是一种链式的结构,相比数组,链表更加灵活,通过指针去进行增、删、改查,但是链表不能通过下标去查询。

一个链表结点包含数据域和指针域。
在这里插入图片描述
看看代码是如何实现: 在一个类中封装了数据,和指针,可以通过创建这个类,实现结点的创建。

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

接下来我们看看什么是 “头结点” 和 “头指针”、尾指针。

头结点:是链表中的第一个结点

头结点分为:

  1. 真实头结点:简单说就是链表第一结点是直接存储数据的结点
  2. 虚拟头结点:其第一结点是数据域为空的链表

头指针:是存储头结点地址的指针。
尾指针:最后一个结点的指针。

知道了链表的特性后,我们来看一下单链表长啥样。
(单链表:由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();
     		}
     	}	
     }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值