线性表的链式存储结构


说到链表,我们先来说一说链表和我们之前所学数组得区别有哪些?

  • 数组存储数据是将元素在内存中连续存放,并且每个内存单元大小也是一致的,查询时是通过数组下标进行查找
  • 链表是与数组相反,存放的顺序是不连续的,并且是通过指针将存储的元素联系到一起的

 另外我们也了解到链表的增删操作相较于数组是比较快的,恰恰相反数组的查改操作比较快。

接下来我们引用官方的定义来对链表做一个简单的了解:

链表是一种物理存储单元上非连续、非顺序的存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

     由官方定义我们可以得到以下几点:

  1. 链表是非连续、非顺序的一种存储结构;
  2. 链表由一系列的结点构成,结点是由数据域和指针域域构成;

接下来我们先来了解几个相关概念(定义)

  • 头节点:顾名思义就是第一个结点(所谓‘头’也就是第一个结点),结点我们有真实头结点(第一个结点存储数据)和虚拟头结点(第一个结点不存储数据)之分,在我们的课程中采用的都是虚拟头结点。

       由图可以看到,第一个结点(头结点)存储的数据是a1,所以这是一个真实的头结点,如果链表为空,就没有结点。

由上图可以看到头结点是一个虚拟结点,它所指向的下一个结点才是这个链表存储的第一个数据,当然如果链表为空,则会有一个结点,也就是虚拟的结点。

  • 头指针(head):它是一个引用变量,用来存储头结点的地址的一个指针
  • 尾指针(rear):和头指针一样,但是是指向链表的最后一个结点的指针

注意:当链表为空,头指针和尾指针均指向头结点(虚拟节点)

 

链表的实现

关于链表的实现是通过实现之前我们在线性表中所写的接口(List)

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

}

首先节点(Node)应该为LinkedList(链表)中的一个内部类,那么什么是内部类?内部类就是在内里面定义一个类,其权限是私有的(private),因为不需要在外部去创建对象。

private class Node{
    	 E data;//数据域
    	 Node next;//指针域
         /**
          *Node的没有参数的构造函数
          */

    	 public Node() {
    		/* data=null;
    		 next=null;*/
    		 this(null,null);
    	 }
          /**
          *Node的带有参数的构造函数
          */
    	 public Node(E data,Node next) {
    		 this.data=data;
    		 this.next=next;
    	 }
         //重新toString方法,用于创建对象时将其打印出来
    	 @Override
    	public String toString() {
    		// TODO Auto-generated method stub
    		return data.toString();
    	}
     } 

关于接口List的实现,具体方法如下:

  • 需要定义的变量有头指针和尾指针,另外还需要一个变量来判断纪录元素的个数。
     private Node head;//指向头结点得头指针
     private Node rear;//指向尾结点得尾指针
     private int size;//纪录元素得个数
  • LinkedList的构造函数,有参数和无参数的构造函数
  1. 因为时虚拟节点,所以先创建一个新的节点,head指向新的结点;
  2. 尾指针和头指针指向的是同一个结点,所以rear=head;
  3. 链表为空,size=0。
     public LinkedList() {
    	 head=new Node();
    	 rear=head;
    	 size=0;
     }
     public LinkedList(E[] arr) {
    	/* head=new Node();
    	 rear=head;
    	 size=0;*/
    	 this();
    	 for(E e:arr) {
    		 addLast(e);
    	 }
  
  • getSize()方法用于获取线性表中的有效元素个数,这里就直接返回size就可以
    @Override
	public int getSize() {
		// TODO Auto-generated method stub
		return size;
	}
  • isEmpty()方法是用来判别线性表是否为空,链表为空的条件有:
  1. 有效元素个数为0,size==0;
  2. 头结点的next(我们称之为‘下一跳’)为空,head.next==null
   @Override
	public boolean isEmpty() {
		// TODO Auto-generated method stub
		return size==0&&head.next==null;
	}
  • add () ——插入方法,插入分为头插入、尾插入和头插和一般插入三种方式,具体实现思路如下:
  1. 头插入(每次插入的结点都在第一个结点)——头插入又分为两种情况:
  • 情况一:链表为空(头指针尾指针都指向头结点)的时候插入一个新的结点(先将元素A封装为新的结点)其中结点的数据域为A元素,指针域为Null,首先我们将头结点的下一跳赋给新结点的下一跳,然后再把新元素的地址给头结点的下一跳,注意尾指针要指向新结点(要移动尾指针)

 

要移动尾指针(空指针插入第一个元素)

 

  • 情况二:在链表有结点(有数据或者有元素)的时候进行头插入,首先我们将头结点的下一跳赋给新结点的下一跳,然后再把新元素的地址给头结点的下一跳,尾指针不需要移动

  2.尾插入——尾插人也分为两种情况:

  •  情况一:链表为空的时候进行尾插入,其原理和头插入是的情况一完全相同
  • 情况二:当链表有数据也就是元素的时候进行尾插人,首先将新元素封装为一个新的结点,然后将新结点的地址赋给尾元素的下一跳,最后尾指针后移至新结点

3.头插和尾插相结合——这种插入方式就是把第一种头插法和第二种尾插法相结合起来,根据实际插入的情况,选择合适的插入方法

4.一般插入——一般插入是指不在头和尾插入,在链表中间插入,或者其他地方(除头和尾),例如下面这个例子要在A和B之间插入D,我们要先找到A结点,然后把A的下一跳给D,然后把D的地址给A的下一跳。

public void add(int index, E e) {
		// TODO Auto-generated method stub
		if(index<0||index>size) {
			throw new IllegalArgumentException("插入角标非法");
		}
		Node n=new Node();
		n.data=e;
		n.next=null;
		if(index==0) {  //头插入
			/*Node n=new Node();
			n.data=e;
			n.next=null;*/
			n.next=head.next;
			head.next=n;
			if(size==0) {
				rear=n;
			}
//			size++;
			/*if(size==1) {
				rear=n;
			}*/
			
		}else if(index==size){  //尾插人
			/*Node n=new Node();
			n.data=e;
			n.next=null;
			rear.next=n;*/
//			rear.next=new Node(e,null);
			rear.next=n;
			rear=rear.next;
//			size++;
		}else {  //其他插入
			Node p=head;
			for(int i=0;i<index;i++) {
				p=p.next;
			}
			n.next=p.next;
			p.next=n;
//			size++;
		}
		size++;
	}
  • addFirt()和 addLast()——在表头插入新元素和在表尾插入新元素,在表头插入元素是在角标0处,在表尾插入元素是在角标size处
@Override
	public void addFirst(E e) {
		// TODO Auto-generated method stub
		add(0,e);
		}

	@Override
	public void addLast(E e) {
		// TODO Auto-generated method stub
		add(size,e);
	}
  • get()——获取链表中的元素,分为指定元素、表头元素和表尾元素,在获取指定元素的时候分为三种(表头、表尾和其他),每种情况的代码实现如下:
@Override
	public E get(int index) {
		// TODO Auto-generated method stub
		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;
		}
		
	}
   //查找头元素,直接为get(0)
	@Override
	public E getFirst() {
		// TODO Auto-generated method stub
		return get(0);
	}

	@Override
	public E getLast() {
		// TODO Auto-generated method stub
		return get(size-1);
	}
  • set()——修改角标,在线性表中其方法定义修改线性表中指定index处的元素为所有新元素,在链表中同样也是这样的,在这个方法中同样分为三种情况(头,尾和其他),思路是和上面的get()方法是非常雷同的,具体代码实现如下:
@Override
	public void set(int index, E e) {
		// TODO Auto-generated method stub

		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;
		}
		
	}
  • contains()和find()——这两个方法都是查找的方法,其中contains()方法的实现是建立在find()方法基础之前,我们先来说一说这两个方法的含义都是什么,contains() 判表中是否包含指定元素e 默认从前往后找,find()在表中获取指定元素e的角标 默认是从前往后找,关于find()方法的实现主要分为几个步骤:首先判断链表是否为空,如果为空,则返回-1;如果不为空,我们要遍历链表中的元素(循环),最后返回角标。因此contains()方法的实现只要满足find()方法中的不返回-1即可,通俗讲就是只要不为空,肯定能找到指定的元素。
public boolean contains(E e) {
		// TODO Auto-generated method stub
		return find(e)!=-1;
	}

	@Override
	public int find(E e) {
		// TODO Auto-generated method stub
		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;
	}
  • remove() ——删除的方法与插入一样也分为头删、尾删和一般删,具体实现思路如下:
  1. 头删:头删顾名思义删除第一个结点,把要删除的结点的下一跳给头(head)的下一跳即可,但是现在所删除的结点依然指向它的下一个结点,所以我们需要把它的next情况,也就是为null,当然也可以不清,因为Java中有垃圾回收机制。如下图我们要删除A,我们需要把A的下一跳给head的下一跳,然后A的下一跳赋值为null:

       但是有个特殊情况就是要删除结点的时候,链表中只要一个结点(数据),这时候我们要把头结点的下一跳为null(这个时候要删除的结点的下一跳为null,把要删除的结点的下一跳给头结点的下一跳),尾指针需要移动到头结点

2.尾删:尾删就是删除链表的尾结点,我们需要先找到尾结点的上一个节点,然后把其下一跳赋为null,然后尾指针移到其位置上,(或者先移动尾指针,然后再将下一跳赋null)这里没有特殊情况,因为删除尾结点,尾指针一直在移动

3.一般删除:一般删除就是除了删除头结点和尾结点,删除链表中的其他结点,如下图,要删除结点B,我们要先找到A结点(要删除结点的上一个节点),然后将B的下一跳赋给A的下一跳

@Override
	public E remove(int index) {
		// TODO Auto-generated method stub
		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;
			}
			/*E res=p.next.data;
			p.next=p.next.next;*/
		
			Node del=p.next;
			res=del.data;
			p.next=del.next;
			del.next=null;
			rear=p;
			
			
					
		}
		size--;
		return res;
	
	}

	@Override
	public E removeFirst() {
		// TODO Auto-generated method stub
		return remove(0);
	}

	@Override
	public E removeLast() {
		// TODO Auto-generated method stub
		return remove(size-1);
	}
  •  removeElement()——删除表中的指定元素,通过调用上面的删除方法,给定角标即可完成删除操作
@Override
	public void removeElement(E e) {
		// TODO Auto-generated method stub
		int index=find(e);
		if(index<0||index>=size) {
			throw new IllegalArgumentException("删除角标非法");
		}
		remove(index);
	}
  • clear()——清空操作,相当于将头结点的下一跳为null,尾指针指向头结点,同时size=0
@Override
	public void clear() {
		// TODO Auto-generated method stub
		head.next=null;
		rear=head;
		size=0;
	}
  •  toString()的重写——先创建一个字符流StringBuilder对象,然后需要先判定链表是否为空,如果为空直接打印“[]”,如果不为空需要拼接字符串,先打印“[”,然后将元素遍历出来,想要将元素打印出来,在遍历过程中我们还要做一个判断就是是否是最后一个元素,如果是打印“]”,如果不是打印“,”。
    @Override
	public String toString() {
		// TODO Auto-generated method stub
		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();
	}
  • equals()——equals方法是和我们之前所讲的线性表的equals方法一样,所以完全可以拿过来用
@Override
	public boolean equals(Object obj) {
		// TODO Auto-generated method stub
		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;
	}

测试类的编写

public static void main(String[] args) {
		// TODO Auto-generated method stub
       LinkedList<Integer> list=new LinkedList<Integer>();
       for(int i=1;i<=10;i++) {
    	   list.addFirst(i);
       }
       System.out.println(list);
       for(int i=11;i<=15;i++) {
    	   list.addLast(i);
       }
       System.out.println(list);
       System.out.println(list.getFirst());
       System.out.println(list.getLast());
       for(int i=1;i<=5;i++) {
    	   list.removeFirst();
    	   list.removeLast();
       }
      System.out.println(list);
       list.removeFirst();
       list.removeFirst();
       list.removeFirst();
       list.removeFirst();
       list.removeFirst();
       System.out.println(list);
       for(int i=1;i<=10;i++) {
    	   list.addFirst(i);
       }
       System.out.println(list);
       System.out.println(list.getFirst());
       System.out.println(list.getLast());
}

运行结果:


版权声明:本文为qq_44941119原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。