(一)链表与数组, 栈,队列的区别
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
数组栈容易进行数组的扩容操作,将数组元素进行拷贝
*
* */
}