数据结构(三)--链表

(一)链表与数组, 栈,队列的区别

1.动态数组,栈,队列。这三种数据结构底层依托的静态数组,靠resize解决固定容量问题。
2.链表是真正的动态数据结构
特点:
1.链表是最简单的动态数据结构
2.更深入的理解引用(或者指针)
3.更深入的理解递归
4.辅助组成其他数据结构

(二)链表

链表(Linked List)

1.数据存储在节点(Node)中
class Node{
E e;           存储的数据
Node next;     指向下一个节点的引用
}

若链表的next中存储的值为null,说明这个节点是链表的尾节点;

由此看来:
链表的优点:可以实现真正的动态,不需要处理固定容量的问题
链表的缺点:相比较于数组来说,链表丧失了随机访问(由索引取值)的能力
数组和链表的对比:

1.数组最好用于索引有语意的情况,score[2],比如此处可以将索引设置为学号,这样就可以直接取对应学号的学生的成绩最大的优点:支持快速查询

2.链表不适用于索引有语意的情况,最大的优点:动态

(三)LinkedList的底层实现

package cn.data.LinkedList;

public class LinkedList<E> {
	
	//将节点Node设置成LinkedList的内部类,并将其私有化
	//对外部用户屏蔽底层的实现细节,用户只需要知道有哪些可以执行的方法就行
	private class Node{
		public E e;    //存放元素
		public Node next;   //存放指向下一节点的引用
	//此处将E和Node都用public修饰,可以在LinkedList中随意对其进行访问和修改
	//当然也就不需要设置专门的get和set方法
		
	//构造函数
	public Node(E e,Node next){
		this.e = e;
		this.next = next;
	}
	
	//设置一些更简单的构造方法
	public Node(E e){
		this(e,null);
	}
	
	public Node(){
		this(null,null);
	}
	
	
	@Override
	public String toString(){
		return e.toString();
	}
	
	
	}
	
	//private Node head;//头结点
	private Node dummyHead;//虚拟的头结点
	private int size;   //记录链表中的元素个数.用private修饰,防止用户从外部访问时修改
	
	public LinkedList(){
		//初始化时,链表中一个元素也没有
		//head = null;
		dummyHead = new Node(null,null);
		size = 0;
	}
	
	//获取链表中元素的个数
	public int getSize(){
		return size;
	}
	
	//返回链表是否为空
	public boolean isEmpty(){
		return size==0;
	}
	
	/*//在链表头添加元素e(没有给链表设置虚拟节点的时候)
	public void addFirst(E e){
		//创建一个节点
		Node node = new Node(e);
		//将要添加的元素的下一个节点指向原来链表的头部节点
		node.next = head;
		//更新链表的头部节点
		head = node;
		//维护一下size变量,链表中新增了一个节点
		
		//以上三行代码可以写为一行
		//head = new Node(e,head);
		
		size++;
	}
	*/
	//在链表节点增加新的元素e
	//时间复杂度为O(1)
	public void addFirst(E e){
		add(0,e);
	}
	
	
	//在链表的index(0-based从0开始)位置添加一个元素e(关键:找到要添加的节点的前一个节点prev)
	//插入操作就是在prev节点和index节点之间插入,这样新插入的节点的索引为index。
	//这种操作很少用,因为当我们选择使用链表时,一般都不会选择使用索引
	
	//时间复杂度O(n/2)=O(n),相当于平均在中间索引位置添加
	public void add(int index,E e){
		//判断索引的合法性
		if(index<0 || index>size){
			throw new IllegalArgumentException("Add failed,Illegal index.");
		}
		//在链表的头部插入节点,由于链表头部没有前一个元素,需特殊对待
		//if(index == 0){
		//	addFirst(e);
		//}else{
			//找出index位置(index-1)的前一个元素
			//将prev从头结点开始进行遍历
		//Node prev = head;
			Node prev = dummyHead;
			//for(int i =0;i<index-1;i++){
			//有了虚拟节点后,是从dummyHead开始遍历的,次数比没有虚拟节点的时候多一次
			for(int i =0;i<index;i++){
				prev = prev.next;
			}
			
			Node node = new Node(e);
			//将原先链表中prev节点后面的节点赋给要插入的节点的下一个节点
			//在prev.next和新插入的节点node之间建立联系
			node.next = prev.next;
			//在prev和node之间建立联系
			prev.next = node;
			
			//prev.next = new Node(e,prev.next);
			size++;
		//}		
	}
	//在链表中添加一个元素的操作在头节点有特殊性,因为头结点不存在前一个节点,
	//是否可以将其进行优化
	//我们可以设置一个虚拟的头结点(dummyHead),这个虚拟的头结点中存的值为null,并且指向第一个节点
	//这样就可以不用对在头节点增加元素这种情况进行特殊处理
	
	//在链表末尾添加新的元素e
	//时间复杂度为O(n),因为必须遍历到链表尾部才能对链表进行添加操作
	public void addLast(E e){
		add(size,e);
	}
	
	
	//获得链表的第index(0-based)个位置的元素
	//在链表中不是一个常用的操作
	
	//时间复杂度为O(n)
	public E get(int index){
		//对index的合法性进行校验
		if(index<0 || index>=size){
			throw new IllegalArgumentException("Get failed,Illegal index");
		}
		//遍历链表,此处遍历的是当前(current/cur)索引上的元素
		Node cur = dummyHead.next;
		for(int i=0;i<index;i++){
			cur = cur.next;
		}
		return cur.e;	
	}
	
	//获得链表的第一个元素
	public E getFirst(){
		return get(0);
	}
	
	//获得链表的最后一个元素
	public E getLast(){
		return get(size-1);
	}
	
	//修改链表的第index(0-based)个未知的元素为e
	//时间复杂度为O(n),由于不支持随机访问,所以修改操作必须先找到元素才能修改
	public void set(int index,E e){
		//传入index合法性校验
		if(index<0 || index>=size){
			throw new IllegalArgumentException("Update failed,Illegal index");
		}
		
		//遍历,找到第index位置的元素
		Node cur = dummyHead.next;
		for(int i=0;i<index;i++){
			cur = cur.next;
		}
		cur.e = e;	
	}
	
	
	//查找链表中是否有元素e
	//时间复杂度为O(n)
	public boolean contains(E e){
		//没有索引,需要从头开始遍历
		Node cur = dummyHead.next;
		//如果cur!=null说明当前节点是一个有效节点
		while(cur!=null){
			if(cur.e.equals(e)){
				return true;	
			}
			//如果没找到,继续遍历下一个
				cur = cur.next;
		}
		//当整个链表遍历结束还没找到,返回结果
			return false;
	}
	
	
	@Override
	public String toString(){
		
		StringBuilder res = new StringBuilder();
		
		Node cur = dummyHead.next;
		while(cur!=null){
			res.append(cur+"->");
			cur = cur.next;
		}
		
		/**
		 * 遍历的另外一种写法
		 * for(Node cur = dummyHead.next;cur!= null;cur = cur.next){
		 * res.append(cur +"->");
		 * }
		 * */
		res.append("NULL");//表示遍历结束
		
		return res.toString();
	}
	
	//删除链表元素,关键是找到要删除的节点(delNode)的前一个节点(prev)
	 
	//从链表中删除index(0-based)位置的元素,返回删除的元素
	//时间复杂度为O(n/2)=O(n)
	public E remove(int index){
		//进行index合法性校验
		if(index<0 || index>= size){
			throw new IllegalArgumentException("Remove failed,Index is illegal");
		}
		
		//找到带删除元素之前的节点
		Node prev = dummyHead;
		for(int i=0;i<index;i++){
			prev = prev.next;
		}
		//返回删除的元素
		Node retNode = prev.next;
		prev.next = retNode.next;
		retNode.next = null;
		
		size--;
		return retNode.e;	
	}
	
	//从链表中删除第一个元素,返回删除的信息
	//时间复杂度为O(1)
	public E removeFirst(){
		return remove(0);
	}
	
	//从链表中删除最后一个元素,返回删除的元素
	//时间复杂度为O(n),因为必须先找到要删除的元素的前一个节点
	public E removeLast(){
		//最后一个元素的索引为size-1.
		return remove(size-1);
	}
	//总的来说:链表的增删改查四个操作的时间复杂度为O(n)	
	//如果只是对链表头进行操作,只查链表头的元素,不进行链表的改操作,则链表的时间复杂度为O(1)
}
测试链表的增删改查功能:
0->NULL
1->0->NULL
2->1->0->NULL
3->2->1->0->NULL
4->3->2->1->0->NULL
4->3->666->2->1->0->NULL
4->3->2->1->0->NULL
3->2->1->0->NULL
3->2->1->NULL

(四)用链表实现栈

package cn.data.LinkedList;

import cn.data.Stack.Stack;

public class LinkedListStack<E>  implements Stack<E> {
	
	private LinkedList<E> list;
	
	public LinkedListStack(){
		list = new LinkedList<>();
	}
	
	@Override
	public int getSize(){
		return list.getSize();
	}
	
	@Override
	public boolean isEmpty(){
		return list.isEmpty();
	}
	
	@Override
	public void push(E e){
		list.addFirst(e);
	}
	
	@Override
	public E pop(){
		return list.removeFirst();
	}
	
	@Override
	public E peek(){
		return list.getFirst();
	}
	//对于链表来说,链表的头是栈顶
	@Override
	public String toString(){
		
		StringBuilder res = new StringBuilder();
		res.append("Stack:top");
		res.append(list);
		return res.toString();
	
	}
	
	//封装了一个底层是链表的链表栈
	
	public static void main(String[] args) {
		
		LinkedListStack<Integer> stack = new LinkedListStack<>();
		
		for(int i=0;i<5;i++){
			stack.push(i);
			System.out.println(stack);
		}
		stack.pop();
		System.out.println(stack);
	}	
}
Stack:top0->NULL
Stack:top1->0->NULL
Stack:top2->1->0->NULL
Stack:top3->2->1->0->NULL
Stack:top4->3->2->1->0->NULL   元素从栈顶入栈
Stack:top3->2->1->0->NULL     出栈也从栈顶出栈

(五)用链表实现队列

package cn.data.LinkedList;

import cn.data.Queue.Queue;

public class LinkedListQueue<E> implements Queue<E>{
	
	private class Node{
		public E e;    
		public Node next;   
	
	public Node(E e,Node next){
		this.e = e;
		this.next = next;
	}
	
	//设置一些更简单的构造方法
	public Node(E e){
		this(e,null);
	}
	
	public Node(){
		this(null,null);
	}
	
	
	@Override
	public String toString(){
		return e.toString();
	}
}
	
private Node head,tail;
private int size;

//此构造函数和默认的构造函数初始化功能一样,但将其显式化
public LinkedListQueue(){
	head = null;
	tail = null;
	size = 0;
}	

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

@Override
public boolean isEmpty(){
	return size == 0;
}

//入队操作
@Override
public void enqueue(E e){
	//tail为空说明head也为空,整个链表都为空
	if(tail == null){
		//在尾部插入一个节点
		tail = new Node(e);
		head = tail;
	}else{
		//否则的话,就在tail节点后面添加元素就可以了
		tail.next = new Node(e);
		//维护tail
		tail = tail.next;
	}
	//维护size
	size++;
}

//出队操作
@Override
public E dequeue(){
	//如果对列为空,就没法进行出队操作
	if(isEmpty()){
		throw new IllegalArgumentException("Cannot dequeue from an empty queue");
	}
	//出队的元素所在的节点就是head所指向的节点
	Node retNode = head;
	//原来链表head的下一个节点更新为现在链表的头结点
	head = head.next;
	//将原来的head节点与链表断开
	retNode.next = null;
	//如果链表中只存在一个元素,那么一个元素出队之后,head节点会指向null
	if(head == null){
		tail = null;
		//在只有一个元素的链表中,head节点和tail节点都是指向该元素
		//若果不进行tail == null,那么tail 此时还是指向的头结点(要返回的节点),这是错误的
	}
	//有元素出队,注意维护size
	size--;
	return retNode.e;
}

//获得队首元素
@Override
public E getFront(){
	if(isEmpty()){
		throw new IllegalArgumentException("Queue is Empty");
	}
	return head.e;
}

@Override
public String toString(){
	StringBuilder res = new StringBuilder();
	res.append("Queue: front ");
	
	Node cur = head;
	while(cur !=null){
		res.append(cur + "->");
		cur = cur.next;
	}
	res.append("NULL tail");
	return res.toString();
}


public static void main(String[] args) {
	
	LinkedListQueue<Integer> queue = new LinkedListQueue<>();
	for(int i=0;i<10;i++){
		queue.enqueue(i);
		System.out.println(queue);
		if(i%3 == 2){
		queue.dequeue();
		System.out.println(queue);
	}
	}	
}

}
测试结果:
Queue: front 2->3->4->5->NULL tail
Queue: front 2->3->4->5->6->NULL tail
Queue: front 2->3->4->5->6->7->NULL tail
Queue: front 2->3->4->5->6->7->8->NULL tail       元素从尾部添加
Queue: front 3->4->5->6->7->8->NULL tail          元素从头部删除
Queue: front 3->4->5->6->7->8->9->NULL tail

(六)ArrayStack与LinkedListStack的时间复杂度比较
package cn.data.LinkedList;

import java.util.Random;
import cn.data.Stack.ArrayStack;
import cn.data.Stack.Stack;

public class testFunction {
	
	//测试使用q运行opCount个enqueue和dequeue操作所需要的时间,单位:秒
		private static double testStack(Stack<Integer> stack,int opCount){
			//nanoTime返回的是纳秒级别的时间
			long startTime = System.nanoTime();
			
			Random random = new Random();
			for(int i=0;i<opCount;i++){
				//生成0到int间最大的数进行入队操作
				stack.push(random.nextInt(Integer.MAX_VALUE));
			}
			
			for(int i=0;i<opCount;i++){
				stack.pop();
			}
			long endTime = System.nanoTime();
			//纳秒级数据不易阅读,将其转换为秒级别的
			return (endTime -startTime)/1000000000.0;
		}
		
		public static void main(String[] args) {
		
			int opCount = 100000;
			
			ArrayStack<Integer> arrayStack = new ArrayStack<>();
			double time1 = testStack(arrayStack,opCount);
			System.out.println("ArrayStack,time:"+time1+"s");
			
			LinkedListStack<Integer> linkedListStack = new LinkedListStack<>();
			double time2 = testStack(linkedListStack,opCount);
			System.out.println("LinkedListStack,time:"+time2+"s");
			
			/*LinkedListQueue<Integer> linkedListQueue = new LinkedListQueue<>();
			double time3 = testStack(linkedListQueue,opCount);
			System.out.println("linkedListQueue,time:"+time3+"s");*/
		}
  //其实这个时间比较复杂,因为LinkedListStack中包含更多地new Node操作
/**两者的时间复杂度一样,耗时并不一定谁小
 * ArrayStack,time:0.024338015s
LinkedListStack,time:0.016278217s
数组栈容易进行数组的扩容操作,将数组元素进行拷贝
 * 
 * */
}


猜你喜欢

转载自blog.csdn.net/jaybillions/article/details/80918210