0035数据结构之链表

---------------------------------------链表-动态数据结构--------------------------------------------------------------

链表的头节点指向第一个元素,但是为头节点前添加元素的时候,找头节点前的元素时和其他元素找前边的元素时比,比较特殊,所以我们在头节点前设置一个虚拟头节点dummyHead

1)  创建内部节点类Node,含有元素对象E e和包含的下一个节点Node next

2)  创建链表的两个属性虚拟头节点dummyHead和链表大小size;

3)  链表的三个构造函数,先创建两个参数的构造函数,其他两个构造函数调用该构造函数

4) getSize()和isEmpty()方法

5) add(int index,E e)方法以及addFirst(E e)和addLast(E e)方法

6) 查找index位置的元素:E get(index)以及getFirst()和getLast()

7) 修改index位置的元素:set(int index,E e)

查找链表中是否有元素e: boolean contains(E e)

8) 重写toString()方法

9) 删除方法:E remove(int index)和removeFirst()、removeLast();

package linked;

public class LinkedList {
    //定义内部类,该类代表节点,用于存放对象数据,以及下一个节点数据
   
//下一个数据,其实不应该理解为指向的数据,更应该理解为递归包含数据
   
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);
        }
    }

    //虚拟头节点,不存放任何元素,方便处理链表的第一个元素(真正的头节点)时与处理其他元素无异
   
private Node dummyHead;
    private int size;
    //链表的构造函数
   
public LinkedList(){
        dummyHead = new Node(null,null);
        size = 0;
    }

    //获取链表中元素的个数
   
public int getSize(){
        return  size;
    }

    //判断链表是否为空
   
public boolean isEmpty(){
        return size==0;
    }

    //在指定位置加入节点
   
public void add(int index,E e){
        if(index < 0 || index > size){
            throw new IllegalArgumentException("add failed.index is illegal.");
        }
        //找到欲加入位置的前一个节点
       
Node prev = dummyHead;
        for(int i=0;i             prev = prev.next;
        }
        //插入节点,相当于两步操作,index前一个节点的next指向新的节点,创建的新的节点的next指向原index位置的节点
       
prev.next = new Node(e,prev.next);
        //新增或者删除节点都需要改变链表的size
       
size++;
    }

    //在链表头部加入节点,链表的第一个真正含有数据的元素是从0开始算的
   
public void addFirst(E e){
        add(0,e);
    }

    //在链表的尾部加入节点(si既代表链表元素的个数,也代表待插入元素的位置)
   
public void addLast(E e){
        add(size,e);
    }

    //获取第index位置的节点中的元素数据
   
public E get(int index){
        //注意此处如果index等于size也是不合法的,因为size是指向待插入元素的位置
       
if(index < 0 || index >= size){
            throw new IllegalArgumentException("add failed.index is illegal.");
        }
        Node current = dummyHead;
        //如果index==0,则需要遍历一次,从dummyHead指向第一个Node,如果index==1,则需要遍历两次,才能指向1位置的元素,依次类推
       
//所以此处判断条件要用<=index
       
for(int i=0;i<=index;i++){
            current = current.next;
        }
        //直接用current.e,这也是Node中属性E设置为public访问范围的原因
       
return current.e;
    }

    //获取链表的第一个元素
   
public E getFist(){
        return get(0);
    }

    //获取链表的最后一个元素
   
public E getLast(){
        return get(size-1);
    }

    //修改链表第index位置的元素
   
public void set(int index,E e){
        //注意此处如果index等于size也是不合法的,因为size是指向待插入元素的位置,逻辑与查找index位置的逻辑相同
       
if(index < 0 || index >= size){
            throw new IllegalArgumentException("add failed.index is illegal.");
        }
        Node current = dummyHead;
        for(int i=0;i<=index;i++){
            current = current.next;
        }
        current.e = e;
    }

    //查找链表中是否有元素e
   
public boolean contains(E e){
        Node current = dummyHead;
        //遍历链表的第一种方式
       
/*for(int i=0;i             current =current.next;
            if(e.equals(current.e)){
                return true;
            }
        }*/
        //
遍历链表的第二种方式
       
while(current.next != null){
            current = current.next;
            if(e.equals(current.e)){
                return true;
            }
        }
        return false;
    }

    //删除指定索引位置的元素
   
public E remove(int index){
        if(index < 0 || index >= size){
            throw new IllegalArgumentException("add failed.index is illegal.");
        }
        Node prev = dummyHead;
        for(int i=0;i             prev = prev.next;
        }
        Node indexNode = prev.next;
        //通过下边的两步断开索引的前后连接
       
//1索引位置前一个nodenext不再指向索引位置的元素,而是指向索引的下一个元素
       
prev.next = indexNode.next;
        //2将原索引位置的下一个元素设置为null
        
indexNode.next = null;
        size--;
        return indexNode.e;
    }

    //删除链表中的第一个元素
   
public E removeFirst(){
        return remove(0);
    }

    //删除链表中的最后一个元素
   
public E removeLost(){
        return remove(size-1);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("LinkedList: size = %d\n",size));
        Node current = dummyHead;
        for(int i=0;i<size;i++){
           current = current.next;
           sb.append(current.e);
           if(current.next != null){
               sb.append("->");
           }
        }
        return sb.toString();
    }



    public static void main(String[] args) {
        LinkedList linkedList = new LinkedList<>();
        for(int i=0;i<8;i++){
            linkedList.addLast(i);
            System.out.println(linkedList);
        }
        linkedList.add(5,9);
        System.out.println(linkedList);

        linkedList.remove(6);
        System.out.println(linkedList);
        linkedList.removeFirst();
        System.out.println(linkedList);
        linkedList.removeLost();
        System.out.println(linkedList);
    }

}

 

以上方式实现的增删改查方法,都需要从头遍历到第index位置才能再进行相应的操纵,时间复杂读为:

(1 * 1/n + 2 * 1/n + …+ n * 1/n)=(1+n)/2,所以时间复杂度为O(n)

其中1、2、n代表需要遍历的元素的个数,1/n为概率。

所以对于链表的优势是:只在头的位置进行新增和删除,不去进行修改,而查询也只查询头节点的元素,所以在满足这样条件的业务场景,可以考虑使用链表,其实栈就是满足这样的一个场景的,而由于链表是动态的,所以可以节省内存空间。

总结:使用链表实现栈比使用数组实现栈的优势是节省内存空间,且由于链表实现栈不用扩容和缩容,所以整体的效率是优于数组实现栈的。链表栈的缺点是新增元素的时候需要new Node,即创建节点对象,这个创建对象的过程在不同的操作系统或者虚拟机版本中可能由于需要寻找可用的分配给对象的空间,可能会比较耗时。

 

所以对于以上方式实现的链表,其增删改查的时间复杂度都是O(n),整体是劣于数组的,那么如何进行优化呢?

解决办法:增加尾节点

使用链表实现栈:

package linked;

import stack.Stack;

public class LinkedListStack implements Stack {
    LinkedList linkedList;

    public LinkedListStack(){
        linkedList = new LinkedList<>();
    }
    @Override
    public int getSize() {
        return linkedList.getSize();
    }

    @Override
    public boolean isEmpty() {
        return linkedList.isEmpty();
    }

    @Override
    public void push(E e) {
        //链表头的增删改查是O(1)
       
linkedList.addFirst(e);
    }

    @Override
    public E pop() {
        return linkedList.removeFirst();
    }

    @Override
    public E peek() {
        return linkedList.getFist();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        /*int size = linkedList.getSize();
        sb.append(String.format("LinkedListStack: size=%d\n",size));
        sb.append("top:");
        for(int i=0;i             sb.append(linkedList.get(i));
            if(i!=size-1){
                sb.append("->");
            }
        }*/
       
sb.append("LinkedListStack:top:");
        sb.append(linkedList.toString());
        return sb.toString();
    }

    public static void main(String[] args) {
        Stack stack = new LinkedListStack();
        for(int i=0;i<5;i++){
            stack.push(i);
            System.out.println(stack);
        }
        stack.pop();
        System.out.println(stack);
        System.out.println(stack.peek());
    }
}

 

也可写程序测试数组栈和链表栈性能上的差异。

 

使用链表实现队列:

如果直接使用链表实现队列,那么链表尾部的增加和删除元素都是O(1)复杂度的,所以可以考虑在有head标记的同时,增加tail标记,有了tail标记,在tail后增加元素是O(1)的,但是删除元素,由于无法直接找到前一个元素,还需要从头遍历,所以tail端删除元素还是O(n)的,所以当我们想使用链表实现队列的时候,应该用链表的tail端做为队列的队首(增删都是O(1)),使用链表的head端做为队列的队尾(链表本身的head端的增删都是O(1)的)

创建新的链表实现,内部类Node不发生变化,含有head节点和tail节点,以及size变量,其中切记head和tail代表的含义都是指向真是存在的第一个节点和最后一个节点的位置,而不是虚拟节点。

具体代码如下:

package linked;

import queue.Queue;

public class LinkedListQueue implements Queue {
    //定义内部类,该类代表节点,用于存放对象数据,以及下一个节点数据
   
//下一个数据,其实不应该理解为指向的数据,更应该理解为递归包含数据
   
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);
        }
    }

    //headtail都代表真是存在的节点,不代表虚拟节点
   
private Node head;
    private Node 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;
    }

    //在链表尾部添加元素
   
// 因为尾部添加是O(1),尾部无法直接找到上一个元素,需要从头遍历,删除是O(n)
    //
而链表头部新增或者删除都是O(1)
    //
考虑到可以用该链表实现队列,队列一端进(尾部增加),另一端出(头部删除),
   
// 对于删除操作,只有head端能保证O(1),所以链表的head端要做为队列的头部
   
//总结记性,在实现队列的时候,头对头,尾对尾
   
@Override
    public void enqueue(E e){
        //tailnull,代表数组中没有元素,head也为null,所以添加的第一个元素既是尾节点也是头节点
       
if(tail == null){
            tail = new Node(e);
            head = tail;
        }else{
            //如果tail不为null,则原tail.next指向新的节点,然后将新的节点做为尾节点
           
tail.next = new Node(e);
            tail = tail.next;
        }
        //入队出队size都要改变大小
       
size++;
    }

    @Override
    public E dequeue() {
        Node retNode;
        if(isEmpty()){
            throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
        }else{
            retNode = head;
            head = head.next;
            //断开链接,如果不断开链接,相当于只改变了head,并未将原head从队列移除
           
retNode.next=null;
            //如果headnull了,需要维护tail也为null
           
if(head == null){
                tail = null;
            }
            size--;
            return retNode.e;
        }

    }

    @Override
    public E getFront() {
        if(isEmpty()){
            throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
        }else{
            return head.e;
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("LinkedListQueue:head:");
        Node current = head;
        for(int i=0;i<size;i++){
            sb.append(current.e);
            if(i != size-1){
                sb.append("->");
            }
            current = current.next;
        }
        sb.append("tail");
        return sb.toString();
    }

    public static void main(String[] args) {
        LinkedListQueue linkedListQueue = new LinkedListQueue();
        for(int i=0;i<5;i++){
            linkedListQueue.enqueue(i);
            System.out.println(linkedListQueue);
        }
        linkedListQueue.dequeue();
        System.out.println(linkedListQueue);
        System.out.println(linkedListQueue.getFront());
        System.out.println(linkedListQueue);

    }
}

 

至此队列已经有三种实现方式,分别是
1)数组队列

2)循环队列(数组实现的含有head和tail标记,两者都是int类型,标记位置)

3)链表实现(含有头尾节点Node,链表与队列的关系是头对头,尾对尾)

 

数组实现的循环队列与链表实现的含有头尾节点的队列效率是相仿的。

 

 

小总结:数组和链表是最基本的数据结构,可以基于数组或者链表分别实现栈和队列。

 

-------------------------------------链表与递归--------------------------------------------

问题:删除链表中所有值为指定值的元素

两种解决办法:

方法一:程序中对链表的头节点单独特殊处理(Solution类)

方法二:使用虚拟头节点,这样头节点前也有了节点,这样就可以对所有节点进行统一处理了(Solution2类)

 

方法一:

package linked;

import java.util.List;

public class Solution {
    public ListNode removeElements(ListNode head,int val){
        //先处理掉链表的第一个元素,因为这个元素的处理比较特殊
       
//再处理掉链表的非第一个元素
       
//判断如果head不为null,且head是要删除的元素
       
while(head != null && head.val == val){
            ListNode delNode = head;
            head = head.next;
            delNode.next = null;
        }
        //while执行完毕之后,有可能是head为空,也可能是head.val!=val,所以两步都要判断处理
       
if(head == null){
            return head;
        }

        ListNode prev = head;
        while(prev.next != null){
            if(prev.next.val == val){
                //删除节点
               
ListNode delNode = prev.next;
                prev.next = delNode.next;
                delNode.next = null;
            }else{
                //否则后移继续while判断
               
prev = prev.next;
            }
        }
        return head;
    }
}

 

方法二:

package linked;

public class Solution2 {
    public ListNode removeElements(ListNode head,int val){
        //使用虚拟头节点,这样头节点前边也有及诶单了,这样就不用对头节点进行特殊处理了
        
ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;

        ListNode prev = dummyHead;
        while(prev.next != null){
            if(prev.next.val == val){
                //删除节点
               
ListNode delNode = prev.next;
                prev.next = delNode.next;
                delNode.next = null;
            }else{
                //否则后移继续while判断
               
prev = prev.next;
            }
        }
        return dummyHead.next;
    }
}

 

-----------------------------------------------递归算法--------------------------------------------

总结自己递归调用的心得体会,递归算法主要分为3步骤:

1)  终止条件,注意返回类型与函数返回类型相同

2)  调用递归函数,注意参数需要发生变化

3)  计算结果,即计算或者拼接等得到最终需要的返回值

 

递归调用的缺点:反复调用方法,占用栈空间,如果递归次数过多,是会导致栈空间不够用的

 

1、递归算法求int型数组的和:

package recursion;
/*
*
递归调用求数组的和
* 1
、终止条件是什么 递归的方法的返回值是什么,终止条件的返回值就应该是什么
* 2
、递归的规律是什么  arr[0]+arr[1]==arr[index]+arr[index+1],其中index==0
* */
public class Sum {
    public static int sum(int[] arr){
        return sum(arr,0);
    }

    //计算[l,n)范围内数字的和
   
private static int sum(int[] arr,int index){
        //递归结束条件,终止条件的返回值与递归调用的返回值类型相同
       
if(index == arr.length){
            return 0;
        }
        return arr[index]+sum(arr,index+1);
    }


    public static void main(String[] args) {
        int[] arr = {1,5,8,3,18,27};
        System.out.println(sum(arr));
    }

}

 

 

2、递归算法删除链表中的指定值的元素

package linked;

public class Solution3 {
    public ListNode removeElements(ListNode head,int val){
        //1、判断,如果头结点为null,则返回null(者也是递归结束的条件)
       
//2、如果头节点不为空,则递归调用该方法,计算子链表
       
//3、如果头节点是要删除的元素,则返回子链表;如果头节点不是要删除的元素,则用头节点拼接子链表
       
//所以以上三点也满足自己的总结:步骤1是递归结束的条件,步骤二是递归的规律(或者称拆分为更小的问题),
       
// 步骤3是链表的特性导致需要拼接
       
if(head == null){
            return null;
        }else{
            ListNode sonNode = removeElements(head.next,val);
            if(head.val == val){
                return sonNode;
            }else{
                head.next = sonNode;
                return head;
            }
        }
    }
}

 

理解不到之处,望指正!

你可能感兴趣的:(0035数据结构之链表)