Java数据结构与算法中级篇之栈、队列、链表

数据是基础,算法是灵魂

版权声明,本文来自门心叼龙的博客,属于原创内容,转载请注明出处。https://blog.csdn.net/geduo_83/article/details/86466640

源码下载地址:https://download.csdn.net/download/geduo_83/10913510

初级篇:Java数据结构与算法初级篇之数组、集合和散列表
中级篇:Java数据结构与算法中级篇之栈、队列、链表
高级篇:Java数据结构与算法高级篇之树、图

理论篇:Java数组、集合、散列表常见算法浅析
理论篇:Java栈、队列、链表常见算法浅析
理论篇:Java树、图常见算法浅析

1. 概述 
2. 栈
    2.1 什么是栈 
    2.2 栈的存储结构 
    2.3 栈的实现
    2.4 栈的特点
    2.5 适用场景 

3. 队列
    
3.1 什么是队列
    
3.2 队列的存储结构
    
3.3 队列的实现
    
3.4 队列的特点
    3.5 适用场景
4. 链表 
    
4.1 什么是链表
    
4.2 链表的数据结构
    
4.3 链表的特点
   
4.4 使用场景
    4.5 相关算法

1. 概述

在上一篇文章我们讲了数组、集合、散列表,接下来我们来学习数据结构中非常有意思的几个数据结构--栈、队列和链表,这三个结构都有一个共同的特点都是顺序存储数据的,但是他们存储数据的方式不同,各有各的特点,如果我们把上一篇文章学习的数组作为数据结构中幼儿园级别的内容,那么今天这篇文章讲的这三个数据结构相当于小学级别的内容了。这三种数据结构也是我们面试的时候经常被问到的知识点。

2. 栈

2.1 什么是栈

栈是一种只能在一端进行插入和删除的线性数据结构。

2.2 栈的存储结构

Java数据结构与算法中级篇之栈、队列、链表_第1张图片

2.3 栈的实现

我们知道数组是一切数据结构的基础,其他的大部分数据结构都是在数组的基础上实现的,例如上一篇我们讲过的集合,散列表,以及这一篇我们即将讲到的栈和队列。

栈的主要特点就是只能在一端进行数据添加和删除操作的数据结构,它和集合一样离不开两个最基本的操作添加和删除,也就是数据的入栈和出栈,通过一个指针来记录当前下标,入栈就是给下标为size++的这个元素赋值,注意一点当size和源数组的长度相等的时候就给源数组进行扩容。出栈就是取出size--这个元素的值,取出之后要给size--这个位置的元素赋值为0。

package D栈队列.A001用数组实现一个栈;

import java.util.Arrays;

/**
 * Description: <>
* Author: 门心叼龙
* Date: 2018/11/19
* Version: V1.0.0
* Update:
*/ public class MyStack { private int[] arr; private int size = 0; private int initsize = 5; public MyStack() { arr = new int[initsize]; } public void push(int value) { if (size == arr.length) { arr = Arrays.copyOf(arr, size * 2); } arr[size++] = value; } public int pop() { if (size == 0) { throw new IndexOutOfBoundsException("栈里面数组为空了"); } return arr[--size]; } public int size() { return size; } }

2.4 栈的特点

栈的特点是显而易见的,只能在一端进行操作,遵循先进后出或者后进先出的原则。

2.5 适用场景

2.5.1 逆序输出

由于栈有先进后出的特点,所以逆序输出是其中一个非常简单的应用。首先把要存储的元素按顺序入栈,然后把所有的元素都出栈,轻松实现逆序输出。

2.5.2 进制转换

我们可以通过求余法,将十进制转化为其它进制,比如要转为二进制,把十进制数除以2,记录余数0入栈,然后继续将商除以2,记录余数1入栈,一直商等于0为止,最后余数倒着写过来就行了。

Java数据结构与算法中级篇之栈、队列、链表_第2张图片

2.5.3 方法栈

函数调用的过程就是不断的压栈的过程,当函数返回结果的时候就是不断的弹栈的过程。

Java数据结构与算法中级篇之栈、队列、链表_第3张图片

2.5.4 Activity栈

做过Android开发的同学都知道Activity的打开就是一个不断压栈的过程,当我们点击返回键的时候就是一个不断弹栈的过程。

Java数据结构与算法中级篇之栈、队列、链表_第4张图片

 

3. 队列

3.1 什么是队列

队列是一种只能在一端进行插入和在另一端删除的线性数据结构。

3.2 队列的存储结构

Java数据结构与算法中级篇之栈、队列、链表_第5张图片

3.3 队列的实现

栈是入栈、出栈的操作,队列的是入列、出列的操作,我们要实现的是一个循环队列,声明两个指针,一个头指针,一个尾指针,头指针用于数据的入列操作,尾指针用于数据的出列操作。

我们先来分析入列,入列数据能一直加下去吗?不可能的,他是一个循环队列肯定有加满的时候,什么时候满了?当对头指针加1和数组长度取余如果到了尾指针的位置,那么就说明队列满了就直接返回了,否则就给头指针的元素赋值,赋值完毕一定要注意头指针要往前挪动一个位置。因为是循环队列所以移动指针就不是i++那么简单了,而是源指针+1和数组长度取余。

分析完了数据入列,数据出列就简单的多了,直接返回尾指针所指向的数组元素即可,出列注意一点何时队列空了?其实很简单只要头指针和尾指针相等了就说明队列已经没有数据了。尾指针移动的逻辑和头指针一样这里就不在赘述了。

package D栈队列.A002用数组实现一个队列;

/**
 * Description: <>
* Author: 门心叼龙
* Date: 2018/11/19
* Version: V1.0.0
* Update:
*/ public class MyQueue { private int head = 0; private int tail = 0; private final int[] arr; public MyQueue() { arr = new int[5]; } // 塞数据 public boolean put(int value) { if (head == (tail + 1) % arr.length) { System.out.println("队列已经满了..."); return false; } arr[tail] = value; tail = (tail + 1) % arr.length; return true; } public int poll() { if (head == tail) { System.out.println("队列已经空了"); return -1; } int item = arr[head]; arr[head] = 0; head = (head + 1) % arr.length; return item; } }

3.4 队列的特点

先进先出或者后进后出。

3.5 适用场景

3.5.1 排队

只要和排队有关的场景,都可以使用队列的数据结构来解决问题。比如春节抢购火车票、双十一的抢购活动。

3.5.2 Handler消息队列

做过Android开发的都知道,Android系统有一个非常重要的消息队列机制Handler,其数据结构就是一个典型的队列。

Java数据结构与算法中级篇之栈、队列、链表_第6张图片

4. 链表

4.1 什么是链表

链表是由一系列的节点组成的,当前元素都持有对下家元素的引用,栈和队列都是申请一段连续的内存空间,然后进行顺序存储数据,而链表是一种物理上非连续的存储结构,数据元素之间的顺序是通过每个元素的指针关联的。

4.2 链表的数据结构

Java数据结构与算法中级篇之栈、队列、链表_第7张图片

4.3 链表的特点

a.链表在对队头或者队尾进行插入或删除的操作效率都非常高,时间复杂度都是O(1)

a.物理空间不连续,空间开销大。

b.查找元素需要顺序查找,元素越靠后效率越低。

4.4 使用场景

链表的劣势就是查找中间元素时需要遍历,一般而言,链表也经常配合其他的数据结构一起使用,例如散列表、栈、队列等。

4.5 相关算法

4.5.1 实现一个链表

链表的实现首先需要声明一个节点对象,节点对象有两个成员变量,一个是节点的值data,一个是下一个节点Note。

另外就是我们的核心对象链表对象,想一想他都有哪些成员变量?要遍历离不开头节点,要添加一个新元素离不开尾节点,另外我们有时候需要获取一下链表的长度,分析到这里很显然构成链表对象有三个基本的成员变量,头节点,尾节点,长度size。为了简单起见我我们就实现它的一个核心方法添加方法,当在添加一个新节点的时候,首先需要建立一个Note,接着要建立链接关系,如果头节点为空?说明是头一次添加则给头节点赋值,否则把当前节点赋值给尾节点的下家元素,最后一步把当前节点赋值给尾结点就ok了。核心逻辑就是头次给头节点赋值,以后都是当前节点赋值给尾结点的下家节点,并把当前节点赋值给尾结点。不要忘了每次添加一个新的元素都需要给size加1,这样一个简单的链表就实现了,另外需要提供一个返回头结点的方法以便我们对链表进行遍历等操作。

ListNote.java: 

package E链表.A001实现一个链表;

/**
 * Description: <>
* Author: 门心叼龙
* Date: 2018/11/19
* Version: V1.0.0
* Update:
*/ public class ListNode { public int data; public ListNode next; public ListNode(int val) { this.data = val; } public ListNode() { this.data = data; } public void setNext(ListNode next) { this.next = next; } public int getData() { return data; } public void setData(int data) { this.data = data; } public ListNode getNext() { return next; } }

 MyListLink.java:

package E链表.A001实现一个链表;

/**
 * Description: <用数组实现一个链表>
* Author: 门心叼龙
* Date: 2018/11/20
* Version: V1.0.0
* Update:
*/ public class MyListLink { private ListNode first; private ListNode last; private int size; public void addLast(int value) { ListNode listNote = new ListNode(); listNote.setData(value); if (first == null) { first = listNote; } else { last.setNext(listNote); } last = listNote; size++; } public ListNode getListLink() { return first; } }

MainAlgorithm.java

package E链表.A001实现一个链表;

/**
 * Description: <用数组实现一个链表>
* Author: 门心叼龙
* Date: 2018/11/21
* Version: V1.0.0
* Update:
*/ public class MainAlgorithm { public static void main(String[] args) { MyListLink listLink = new MyListLink(); listLink.addLast(1); listLink.addLast(3); listLink.addLast(2); listLink.addLast(11); ListNode headnode = listLink.getListLink(); while (headnode != null) { System.out.println(headnode.data); headnode = headnode.next; } } }

 4.5.2 检查链表有没有环

法1,计数法,原理和我们上一篇文章讲过的判断一个数组里面有没有重复元素的道理是一样的,遍历链表中的每一个元素,如果没有就往Set集合里面塞,有就直接返回了。

法2,差速法,声明两个指针,一个快指针,一个慢指针。遍历链表,快指针一次走两步,慢指针一次走一步,有环则必回相遇。链表遍历的结束条件需要注意一下,快节点不为空且快节点的下家节点也不为空。

package E链表.A002检查链表有没有环;

import java.util.HashSet;
import E链表.A001实现一个链表.ListNode;

/**
 * Description: <检查链表有没有环>
* Author: 门心叼龙
* Date: 2018/11/21
* Version: V1.0.0
* Update:
*/ public class MainAgorithm { public static void main(String[] args) { ListNode listNote = new ListNode(0); ListNode listNote1 = new ListNode(1); ListNode listNote2 = new ListNode(2); ListNode listNote3 = new ListNode(3); listNote.setNext(listNote1); listNote1.setNext(listNote2); listNote2.setNext(listNote3); listNote3.setNext(listNote1); boolean loop = checkLoop(listNote); System.out.println(loop); } // 计数法 public static boolean checkLoop1(ListNode listNote) { HashSet hashSet = new HashSet<>(); ListNode temp = listNote; while (temp != null) { if (hashSet.contains(temp)) { return true; } else { hashSet.add(temp); } temp = temp.next; } return false; } // 差速发 private static boolean checkLoop(ListNode listNote) { ListNode slow = listNote; ListNode fast = listNote; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; // 只要有环必然会相遇 if (fast == slow) { return true; } } return false; } }

4.5.3 查找有环链表的入口节点

其实查找链表入口节点的算法是在判断一个链表有没有环的算法差速法的基础上实现的,如果发现有环则退出循环,接着慢节点回到头节点,然后快节点和慢节点一直往前移动下去,要注意此时快节点和慢节点都是一次挪动一个位置,若相遇则退出循环,此时的慢节点就是我们所要查找的入口节点。

为什么这样操作就能找到入口节点?因为这里面存在一个非常重要的原理,就是从头节点到入口节点的距离等于从相遇节点到入口节点的距离,为什么是这样的,我们来推导一下,我们知道慢指针一次走一步,快指针一次走两步,初始快指针和慢指针都在头节点s0,f0,我们开始移动指针s1,f2 ; s2,f4 ; s3,f6…因此我们得出一个结论,快指针所走的距离始终是慢指针距离的2倍,那么如果设头节点到入口节点的距离为m,从入口节点到相遇节点的距离为x,从相遇节点到入口节点的距离为y,那么当快慢节点相遇时则有2倍的慢节点走的距离等于快节点所走的距离,即就是:2(m+x) = m + x + y + x ; 也就是: 2m + 2x = m + 2x + y ;即可得: m = y;所以当慢节点再次回退到头节点和快节点同时开始移动时,注意了此时快节点一次移动一个节点,那么再次相遇的节点就是循环链表的入口节点。

package E链表.A003查找有环链表的入口节点;

import E链表.A001实现一个链表.ListNode;

/**
 * Description: <查找有环链表的入口节点>
* Author: 门心叼龙
* Date: 2018/11/21
* Version: V1.0.0
* Update:
*/ public class MainAgorithm { public static void main(String[] args) { ListNode listNote = new ListNode(0); ListNode listNote1 = new ListNode(1); ListNode listNote2 = new ListNode(2); ListNode listNote3 = new ListNode(3); listNote.setNext(listNote1); listNote1.setNext(listNote2); listNote2.setNext(listNote3); listNote3.setNext(listNote1); ListNode firstNode = getFirstNode(listNote); System.out.println(firstNode.data); } // 查找有环链表的入口节点 private static ListNode getFirstNode(ListNode listNote) { // 如果链表有环,请找到其入口节点 ListNode slow = listNote; ListNode fast = listNote; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { break; } } if (fast == null || fast.next == null) { return null; } // 重置slow指针 slow = listNote; // 如果没有相遇则继续往下走 // 定理:从头结点到入口节点的距离等于从相遇节点到入口节点的距离***** // 因为:2(m + x) = m + x + y + x; // 所以:2m+2x = m + 2x + y // 得出:m = y; while (slow != fast) { slow = slow.next; fast = fast.next; } return slow; } }

4.5.6 翻转一个链表

这个问题有两种解法,法1指针法,这种方法的主要思想就是通过一个遍历,每走一步把当前节点挪动到新链表的头部即可,法2是数组法,我们主要讲第二种算法,要反转那么就要倒叙遍历,怎么办?对于链表来说很困难,如果是一个数组就方便多了,如果我们将链表的元素存入数组将何如?豁然开朗,对就先将链表元素存入数组,然后在倒叙遍历这个数组,遍历的时候将节点元素依次插入新链表即可。注意了把最后一个节点的next链赋值为null否则会出现循环链表。

package E链表.A004翻转一个链表;

import java.util.ArrayList;
import java.util.List;

import E链表.A001实现一个链表.ListNode;
import E链表.A001实现一个链表.MyListLink;

/**
 * Description: <翻转一个链表>
* Author: 门心叼龙
* Date: 2018/11/21
* Version: V1.0.0
* Update:
*/ public class MainAlgorithm { public static void main(String[] args) { MyListLink link = new MyListLink(); link.addLast(0); link.addLast(1); link.addLast(3); link.addLast(6); ListNode listNote = reverseListNode(link.getListLink()); while (listNote != null) { System.out.println(listNote.getData()); listNote = listNote.next; } } // 指针法 private static ListNode reverseListNode2(ListNode listNote) { // 声明的头指针 ListNode head = listNote; // 申明一个尾指针 ListNode tail = listNote; // 声明一个next指针 ListNode next = listNote.next; // 计算链表的长度 int size = 0; ListNode temp = listNote; while (temp != null) { size++; temp = temp.next; } while (size > 1) { // 缓存一个next ListNode nextNext = next.next; // 更改next的next指针 next.next = head;// 反向了 // 移动head指针的指向 head = next; // 移动next指针的指向 next = nextNext; size--; } // 此时链表有环,干掉环 tail.next = null; return head; } // 数组法 private static ListNode reverseListNode(ListNode listNote) { // 翻转一个链表 ListNode tempNode = listNote; // 把链表的值都放入List集合里面 // 通过翻转数组来翻转集合 List list = new ArrayList<>(); while (tempNode != null) { list.add(tempNode); tempNode = tempNode.next; } ListNode headNode = null; for (int i = list.size() - 1; i >= 0; i--) { if (headNode == null) { headNode = new ListNode(); headNode = list.get(i); headNode.next = list.get(i - 1); } else { if (i == 0) { list.get(i).next = null; } else { list.get(i).next = list.get(i - 1); } } } return headNode; } }

4.5.7 删除链表倒数第N个节点

要删除倒数第n个节点,那么我们只要找到要删除的那个节点前面的那个节点pb,然后再执行pb.next = pb.next.next,这样就轻松的把第n个节点删除了,我们想了,要删除倒数那个节点,我们先找到正数的那个节点指针pa,然后再声明一个指向链表的头部指针pb,以然后两个指针同时移动,直到pa移动到最后,则此时节点pb就是我们要删除那个节点的前面那个节点,我们再执行pb.next = pb.next.next ,同时返回头节点就ok了。

package E链表.A005删除链表中的倒数第N个节点;

import E链表.A001实现一个链表.ListNode;
import E链表.A001实现一个链表.MyListLink;

/**
 * Description: <删除链表中的倒数第N个元素>
* Author: 门心叼龙
* Date: 2018/11/23
* Version: V1.0.0
* Update:
*/ public class MainAlgorithm { public static void main(String[] args){ MyListLink myListLink = new MyListLink(); myListLink.addLast(0); myListLink.addLast(1); myListLink.addLast(2); myListLink.addLast(3); myListLink.addLast(4); //删除倒数第2个元素 ListNode head = removeReIndex(myListLink.getListLink(), 3); while(head != null){ System.out.println(head.data); head = head.next; } } private static ListNode removeReIndex(ListNode listNode, int n) { ListNode head = listNode; ListNode pb = listNode; ListNode pa = listNode; int i = 0; while(i < n ){ pa = pa.next; i++; } while(pa.next != null){ pa = pa.next; pb = pb.next; } pb.next = pb.next.next; return head; } }

4.5.8 合并两个有序链表

简单粗暴一点,直接将两个链表转化为两个数组,然后再将两个数组合并为一个数组,然后对新数组排序,最后再将该数组转化为一个链表即可解决问题。

第二种方法通过指针法,首先声明两个指针,一个头指针,一个尾指针,接着开始遍历两个链表,每走一步比较一下当前的两个元素的大小,如果哪个小就把他添加到尾指针,直到循环完毕,最后一步把不为空的那个链表节点添加到尾指针的屁股后面即可。

package E链表.A006合并两个排好序的链表;

import E链表.A001实现一个链表.ListNode;
import E链表.A001实现一个链表.MyListLink;

/**
 * Description: <合并两个排好序的链表>
* Author: 门心叼龙
* Date: 2018/11/22
* Version: V1.0.0
* Update:
*/ public class MainAlgorithm { public static void main(String[] args) { MyListLink myListLink = new MyListLink(); myListLink.addLast(0); myListLink.addLast(2); myListLink.addLast(4); MyListLink myListLink1 = new MyListLink(); myListLink1.addLast(1); myListLink1.addLast(3); myListLink1.addLast(40); myListLink1.addLast(50); ListNode listNode = mergerListNode(myListLink.getListLink(), myListLink1.getListLink()); while (listNode != null) { System.out.println(listNode.data); listNode = listNode.next; } } public static ListNode mergerListNode(ListNode listNode1, ListNode listNode2) { ListNode head = new ListNode(0); ListNode tail = head; while (listNode1 != null && listNode2 != null) { if (listNode1.data < listNode2.data) { tail.next = listNode1; listNode1 = listNode1.next; } else { tail.next = listNode2; listNode2 = listNode2.next; } tail = tail.next; } tail.next = listNode1 != null ? listNode1 : listNode2; return head.next; } }

源码下载地址:https://download.csdn.net/download/geduo_83/10913510

初级篇:Java数据结构与算法初级篇之数组、集合和散列表
中级篇:Java数据结构与算法中级篇之栈、队列、链表
高级篇:Java数据结构与算法高级篇之树、图

理论篇:Java数组、集合、散列表常见算法浅析
理论篇:Java栈、队列、链表常见算法浅析
理论篇:Java树、图常见算法浅析

问题反馈

有任何问题,请在文章下方给我留言。

关于作者

  var geduo_83 = {
    nickName  : "门心叼龙",
    site : "http://www.weibo.com/geduo83"
  }

 

你可能感兴趣的:(数据结构)