数据结构基础

Posted Polaris

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构基础相关的知识,希望对你有一定的参考价值。

一. 概述

1. 理解

1.1 数据结构与算法的关系

数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构。

程序 = 数据结构 + 算法

数据结构是算法的基础

1.2 线性结构和非线性结构

线性结构

  • 作为最常用的数据结构,特点是数据元素之间存在一对一的线性关系。

  • 包含两种不同的存储结构:顺序存储结构(如数组) 和 链式存储结构(如链表)。

  • 顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的。

  • 链式存储的线性表称为链表,链表的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。

  • 线性结构常见的有:数组,链表,栈,队列,哈希表(散列表)。

非线性结构

  • 树形结构:二叉树,AVL树,红黑树,B树,堆,Trie,哈夫曼树,并查集...
  • 图形结构:邻接矩阵,邻接表...

2. 代码测试工具

2.1 测试某段代码的运行时间

public class TimeUtils 
	private static final SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss.SSS");
	
	public interface Task 
		void execute();
	
	
	public static void test(String title, Task task) 
		if (task == null) return;
		title = (title == null) ? "" : ("【" + title + "】");
		System.out.println(title);
		System.out.println("开始:" + fmt.format(new Date()));
		long begin = System.currentTimeMillis();
		task.execute();
		long end = System.currentTimeMillis();
		System.out.println("结束:" + fmt.format(new Date()));
		double delta = (end - begin) / 1000.0;
		System.out.println("耗时:" + delta + "秒");
		System.out.println("-------------------------------------");
	

2.2 断言工具

public class Asserts 
	public static void test(boolean value) 
		try 
			if (!value) throw new Exception("测试未通过");
		 catch (Exception e) 
			e.printStackTrace();
		
	

2.3 Integer工具

public class IntegerUtils 
	/** 生成随机数 */
	public static Integer[] random(int count, int min, int max) 
		if (count <= 0 || min > max) return null;
		Integer[] array = new Integer[count];
		int delta = max - min + 1;
		for (int i = 0; i < count; i++) 
			array[i] = min + (int)(Math.random() * delta);
		
		return array;
	

	/** 合并两个数组 */
	public static Integer[] combine(Integer[] array1, Integer[] array2) 
		if (array1 == null || array2 == null) return null;
		Integer[] array = new Integer[array1.length + array2.length];
		for (int i = 0; i < array1.length; i++) 
			array[i] = array1[i];
		
		for (int i = 0; i < array2.length; i++) 
			array[i + array1.length] = array2[i];
		
		return array;
		
	

	public static Integer[] same(int count, int unsameCount) 
		if (count <= 0 || unsameCount > count) return null;
		Integer[] array = new Integer[count];
		for (int i = 0; i < unsameCount; i++) 
			array[i] = unsameCount - i;
		
		for (int i = unsameCount; i < count; i++) 
			array[i] = unsameCount + 1;
		
		return array;
	

	/**
	 * 生成头部和尾部是升序的数组
	 * disorderCount:希望多少个数据是无序的
	 */
	public static Integer[] headTailAscOrder(int min, int max, int disorderCount) 
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		
		int begin = (array.length - disorderCount) >> 1;
		reverse(array, begin, begin + disorderCount);
		return array;
	

	/**
	 * 生成中间是升序的数组
	 * disorderCount:希望多少个数据是无序的
	 */
	public static Integer[] centerAscOrder(int min, int max, int disorderCount) 
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		int left = disorderCount >> 1;
		reverse(array, 0, left);
		
		int right = disorderCount - left;
		reverse(array, array.length - right, array.length);
		return array;
	

	/**
	 * 生成头部是升序的数组
	 * disorderCount:希望多少个数据是无序的
	 */
	public static Integer[] headAscOrder(int min, int max, int disorderCount) 
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		reverse(array, array.length - disorderCount, array.length);
		return array;
	

	/**
	 * 生成尾部是升序的数组
	 * disorderCount:希望多少个数据是无序的
	 */
	public static Integer[] tailAscOrder(int min, int max, int disorderCount) 
		Integer[] array = ascOrder(min, max);
		if (disorderCount > array.length) return array;
		reverse(array, 0, disorderCount);
		return array;
	

	/** 升序生成数组 */
	public static Integer[] ascOrder(int min, int max) 
		if (min > max) return null;
		Integer[] array = new Integer[max - min + 1];
		for (int i = 0; i < array.length; i++) 
			array[i] = min++;
		
		return array;
	

	/** 降序生成数组 */
	public static Integer[] descOrder(int min, int max) 
		if (min > max) return null;
		Integer[] array = new Integer[max - min + 1];
		for (int i = 0; i < array.length; i++) 
			array[i] = max--;
		
		return array;
	
	
	/** 反转数组 */
	private static void reverse(Integer[] array, int begin, int end) 
		int count = (end - begin) >> 1;
		int sum = begin + end - 1;
		for (int i = begin; i < begin + count; i++) 
			int j = sum - i;
			int tmp = array[i];
			array[i] = array[j];
			array[j] = tmp;
		
	

	/** 复制数组 */
	public static Integer[] copy(Integer[] array) 
		return Arrays.copyOf(array, array.length);
	

	/** 判断数组是否升序 */
	public static boolean isAscOrder(Integer[] array) 
		if (array == null || array.length == 0) return false;
		for (int i = 1; i < array.length; i++) 
			if (array[i - 1] > array[i]) return false;
		
		return true;
	

	/** 打印数组 */
	public static void println(Integer[] array) 
		if (array == null) return;
		StringBuilder string = new StringBuilder();
		for (int i = 0; i < array.length; i++) 
			if (i != 0) string.append("_");
			string.append(array[i]);
		
		System.out.println(string);
	

二. 复杂度

1. 算法的效率问题

使用不同算法,解决同一个问题,效率可能相差非常大

1.1 求第n个斐波拉契数

  • 斐波那契数列的排列是:0,1,1,2,3,5,8,13,21,34,55,89,144...

  • 它后一个数等于前面两个数的和

public class FibonacciNumber 
    public static void main(String[] args) 
        //耗时:4.674秒
        TimeUtils.test("求第n个斐波那契数:fib1", new TimeUtils.Task() 
            @Override
            public void execute() 
                System.out.println(fib1(45));
            
        );

        //耗时:0.0秒
        TimeUtils.test("求第n个斐波那契数:fib2", new TimeUtils.Task() 
            @Override
            public void execute() 
                System.out.println(fib2(45));
            
        );
    

    /**
     * 实现一:递归
     * 时间复杂度:O(2^n)
     */
    public static int fib1(int n) 
        if (n <= 1) return n;
        return fib1(n - 1) + fib1(n - 2);
    

    /**
     * 实现二:循环
     * 时间复杂度:O(n)
     * <p>
     * 0,1,2,3,4,5,6
     * 0,1,1,2,3,5,8,13
     */
    public static int fib2(int n) 
        if (n <= 1) return n;
        int first = 0, second = 1;
//            int sum = first + second;
//            first = second;
//            second = sum;
              second += first;
              first = second - first;
        
        return second;
    

    /**
     * 实现三:线性代数解法 – 特征方程
     * 时间复杂度:可视为O(1)
     */
    public static int fib3(int n) 
        double c = Math.sqrt(5);
        return (int) ((Math.pow((1 + c) / 2, n) - Math.pow((1 - c) / 2, n)) / c);
    

1.2 度量算法优劣的方法

事后统计

这种方法可行但是有两个问题:

  • 一是要想对设计的算法的运行性能进行评测,需要实际运行该程序。
  • 二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快。

事前估计

通过分析某个算法的时间复杂度,空间复杂度来判断哪个算法更优。

2 时间复杂度

2.1 理解

一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度

T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的T(n) 不同,但时间复杂度相同,都为O(n²)。

2.2 大O表示法

一般用大O表示法来描述复杂度,它表示的是数据规模 n 对应的复杂度。如上述O( f(n) )

忽略常数、系数、低阶

  • 9 => O(1)

  • 2n + 3 => O(n)

  • n^2 + 2n + 6 => O(n^2 )

  • 4n^3 + 3n^2 + 22n + 100 => O(n^3 )

注意:大O表示法仅仅是一种粗略的分析模型,是一种估算,能帮助我们短时间内了解一个算法的执行效率

2.4 对数阶的细节

对数阶一般省略底数:log2(n) = log2(9) * log9(n)

所以 log2(n)、log9(n)统称为logn

2.5 计算时间复杂度的方法

  • 用常数1代替运行时间中的所有加法常数 T(n)=2n²+7n+6 => T(n)=2n²+7n+1

  • 修改后的运行次数函数中,只保留最高阶项 T(n)=2n²+7n+1 => T(n) = 2n²

  • 去除最高阶项的系数 T(n) = 2n² => T(n) = n² => O(n²)

2.6 常见的时间复杂度

  • 常数阶O(1)
  • 对数阶O(logn) //注意:底数不一定是2
  • 线性阶O(n)
  • 线性对数阶O(nlogn)
  • 平方阶O(n^2)
  • 立方阶O(n^3)
  • k次方阶O(n^k)
  • 指数阶O(2^n)

说明

① 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(logn)<Ο(n)<Ο(nlogn) <Ο(n2)<Ο(n3)< Ο(n^k) <Ο(2^n) <Ο(n^n),随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低

② 从图中可见,我们应该尽可能避免使用指数阶的算法

③ 对数阶一般忽略底数,所以log2n,log9n统称logn

2.7 时间复杂度练习

public class TimeComplexityTest 
    public static void test1(int n) 
        // 1
        if (n > 10) 
            System.out.println("n > 10");
         else if (n > 5)  // 2
            System.out.println("n > 5");
         else 
            System.out.println("n <= 5");
        

        // 1 + 4 + 4 + 4
        for (int i = 0; i < 4; i++) 
            System.out.println("test");
        

        // 14 => O(1)
    

    public static void test2(int n) 
        // 1 + 3n => O(n)
        for (int i = 0; i < n; i++) 
            System.out.println("test");
        
    

    public static void test3(int n) 
        // 1 + 2n + n * (1 + 45)
        // => 48n + 1 => O(n)
        for (int i = 0; i < n; i++) 
            for (int j = 0; j < 15; j++) 
                System.out.println("test");
            
        
    

    public static void test4(int n) 
        // n = 8 = 2^3 ,可以执行3次
        // n = 16 = 2^4,可以执行4次
        // => n =  2^k,可以执行log2(n)次

        // log2(n) => O(logn)
        while ((n = n / 2) > 0) 
            System.out.println("test");
        
    

    public static void test5(int n) 
        // log5(n) => O(logn)
        while ((n = n / 5) > 0) 
            System.out.println("test");
        
    

    public static void test6(int n) 
        // i * 2^k = n
        // => k = log2(n/i) = log2(n)

        // 1 + log2(n) + log2(n)
        for (int i = 1; i < n; i = i * 2) 
            // 1 + 3n
            for (int j = 0; j < n; j++) 
                System.out.println("test");
            
        

        // 1 + 2*log2(n) + log2(n) * (1 + 3n)
        // => 1 + 3*log2(n) + 2 * nlog2(n)
        // => O(nlogn)
    

    public static void test7(int n) 
        // 1 + 2n + n * (1 + 3n)
        // => 3n^2 + 3n + 1 => O(n^2)
        for (int i = 0; i < n; i++) 
            for (int j = 0; j < n; j++) 
                System.out.println("test");
            
        
    
    public static void test8(int n,int k) 
        //n
        for(int i = 0;i < n;i++) 
            System.out.println("test");
        
        //k
        for(int i = 0;i < k;i++) 
            System.out.println("test");
        
        //复杂度:O(n+k)
    

2.8 平均时间复杂度和最坏时间复杂度

平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。

最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长

平均时间复杂度和最坏时间复杂度是否一致,和算法本身有关

2.9 均摊复杂度

什么情况下使用均摊复杂度:经过连续的多次复杂度比较低的情况后,出现个别复杂度比较高的情况。

案例:动态数组的扩容

2.10 复杂度震荡

什么是复杂度震荡:在一些特殊的情况下,某个级别的复杂度猛地蹿到了另一个级别,并且持续这一级别不恢复,则说明产生了复杂度震荡。

案例:动态数组扩容倍数、缩容时机设计不得当(扩容倍数*缩容倍数=1),有可能会导致复杂度震荡。

3. 空间复杂度

类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储间,它也是问题规模n的函数。

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况.

在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间。

4. 算法的优化方向

  • 用尽量少的存储空间

  • 用尽量少的执行步骤(执行时间)

  • 根据情况,可以选择空间换时间,也可以时间换空间

三. 线性结构

1. 动态数组ArrayList

1.1 理解

数组是一种顺序存储的线性表,所有元素的内存地址是连续的。

在很多编程语言中,数组都有个致命的缺点:无法动态修改容量。实际开发中,我们更希望数组的容量是可以动态改变的

1.1 属性设计

1.2 接口设计

注意与ArrayList源码对比分析

public interface List<E> 
    /** 元素数量 */
    int size();

    /** 是否为空 */
    boolean isEmpty();

    /** 是否包含某个元素 */
    boolean contains(E element);

    /** 添加元素到末尾 */
    void add(E element);

    /** 获取index位置的元素 */
    E get(int index);

    /** 设置index位置的元素 */
    E set(int index,E element);

    /** 往index位置添加元素 */
    void add(int index,E element);

    /** 删除index位置对应的元素 */
    E remove(int index);

    /** 查看元素的位置 */
    int indexOf(E element);

    /** 清除所有元素 */
    void clear();

1.3 图解方法

添加元素-add(E element)

添加元素-add(int index,E element)

删除元素-remove(int index)

如何扩容

1.4 实现

public class ArrayList<E> implements List<E>
    /** 元素数量 */
    private int size = 0;

    /** 所有元素 */
    private E[] elements;

    /**	默认容量 */
    private static final int DEFAULT_CAPACITY = 10;

    /** 元素未找到返回的下标 */
    private static final int ELEMENT_NOT_FOUND = -1;

    public ArrayList() 
        this(DEFAULT_CAPACITY);
    

    public ArrayList(int capaticy) 
        capaticy = capaticy < DEFAULT_CAPACITY ? DEFAULT_CAPACITY : capaticy;
        elements = (E[])new Object[capaticy];
    

    /**
     * @Description 判断下标是否越界
     */
    private void indexCheck(int index) 
        if(index < 0 || index > size) 
            throw new IndexOutOfBoundsException("Index:" + index + ",Size:" + size);
        
    

    /**
     * @Description 数组容量不够则扩容
     */
    private void ensureCapacity(int size) 
        int oldCapacity = elements.length;
        if(size < oldCapacity) return;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍
        E[] newElements = (E[])new Object[newCapacity];
//        for (int i = 0; i < size; i++) 
//            newElements[i] = elements[i];
//        
        System.arraycopy(elements,0,newElements,0,elements.length);
        elements = newElements;
        System.out.println("扩容:" + oldCapacity + "=>" + newCapacity);
    

    /**
     * @Description 数组容量太多则缩容
     */
    private void trim() 
        int oldCapacity = elements.length;
        if(size >= (oldCapacity >> 1)) return;
        if(oldCapacity <= DEFAULT_CAPACITY) return;
        //剩余空间很多,可以缩容
        int newCapacity = oldCapacity >> 1;
        E[] newElements = (E[])new Object[newCapacity];
//        for (int i = 0; i < size; i++) 
//            newElements[i] = elements[i];
//        
        System.arraycopy(elements,0,newElements,0,elements.length);
        elements = newElements;
        System.out.println("缩容:" + oldCapacity + "=>" + newCapacity);
    

    /**
     * @Description 是否为空
     * @return
     */
    public boolean isEmpty() 
        return size == 0;
    

    /**
     * @Description 元素的数量
     * @return size
     */
    public int size() 
        return size;
    

    /**
     * @Description 往index位置添加元素
     * @param index
     * @param element
     * @return
     */
    public void add(int index, E element) 
        //最好复杂度:O(1)、最坏复杂度:O(n)、平均复杂度:O(n)
        indexCheck(index);
        ensureCapacity(size);
        for(int i = size;i > index;i--) 
            elements[i] = elements[i - 1];
        
        elements[index] = element;
        size++;
    

    /**
     * @Description 添加元素到最后面
     * @param element
     */
    public void add(E element) 
        //最好:O(1)
        //最坏:O(n)  => 扩容的情况
        //平均:O(1)
        //均摊复杂度:O(1)  =>把扩容情况均摊到每一种情况去
        //         (一般均摊等于最好)。
        //什么情况下使用均摊复杂度:经过连续的多次复杂度比较低的
        //         情况后,出现个别复杂度比较高的情况。
        add(size, element);
    

    /**
     * @Description 删除index位置对应的元素
     * @param index
     * @return oldEle
     */
    public E remove(int index) 
        //最好复杂度:O(1)、最坏复杂度:O(n)、平均复杂度:O(n)
        indexCheck(index);
        E oldEle = elements[index];
        if(index != size - 1) 
            for(int i = index;i < size;i++) 
                elements[i] = elements[i + 1];
            
        
        elements[--size] = null;//内存管理细节

        trim(); //内存紧张考虑缩容
        return oldEle;
    

    /**
     * @Description 删除某个元素
     * @param element
     */
    public void remove(E element)  //O(1)
        remove(indexOf(element));
    

    /**
     * @Description 设置index位置的元素
     * @param index
     * @param element
     * @return
     */
    public E set(int index, E element)   //O(1)
        indexCheck(index);
        E old = elements[index];
        elements[index] = element;
        return old;
    

    /**
     * @Description 返回index位置对应的元素
     * @param index
     * @return
     */
    public E get(int index) 
        indexCheck(index);
        return elements[index];
    

    /**
     * @Description 查看元素的位置
     * @param element
     * @return
     */
    public int indexOf(E element) 
        if(element == null) 
            for (int i = 0; i < size; i++) 
                if(elements[i] == null) return i;
            
         else 
            for (int i = 0; i < size; i++) 
                if(element.equals(elements[i])) return i;
            
        
        return ELEMENT_NOT_FOUND;
    

    /**
     * @Description 是否包含某个元素
     * @param element
     * @return
     */
    public boolean contains(E element) 
        return indexOf(element) != ELEMENT_NOT_FOUND;
    

    /**
     * @Description 清除所有元素
     */
    public void clear() 
        //方法一:只是访问不到了,数组每一个位置对应的对象还存在。
        //       当对某个位置再次add操作时,此位置存储的地址值对
        //       应以前的对象才会被销毁。
        //size = 0;
        //方法二:对每一个位置对应的对象地址值置空(内存管理细节)
        for (int i = 0; i < size; i++) 
            elements[i] = null;
        
        size = 0;
    

    @Override
    public String toString() 
        StringBuilder str = new StringBuilder();
        str.append("size=").append(size).append(" : [");
        for (int i = 0; i < size; i++) 
            if(i != 0) str.append(", ");
            str.append(elements[i]);
        
        str.append("]");
        return str.toString();
    

2. 单向链表LinkedList

2.1 理解

动态数组有个明显的缺点:可能会造成内存空间的大量浪费。能否用到多少就申请多少内存?链表可以办到这一点。

链表存储结构的特点:

  • 链表是一种链式存储的线性表,通过指针域描述数据元素之间的逻辑关系,不需要地址连续的存储空间。
  • 动态存储空间分配,即时申请即时使用。
  • 访问第i个元素,必须顺序依此访问前面的1 ~ i-1的数据元素,也就是说是一种顺序存取结构。
    插入/删除操作不需要移动数据元素。

注意:

① Java中如何实现“指针”Java中的对象引用变量并不是存储实际数据,而是存储该对象在内存中的存储地址。

② 链表分为带头节点的链表和没有头节点的链表,根据实际的需求来确定。

2.2 图解方法

2.3 实现

class SingleLinkedList<E> implements List<E>
	/**
	 *	元素的数量
	 */
	private int size;
	
	/**
	 *  指向第一个节点的指针
	 */
	private Node<E> first;

	/**
	 * 	元素未找到返回的下标
	 */
	private static final int ELEMENT_NOT_FOUND = -1;

	/**
	 * @Description 判断下标是否越界
	 */
	private void indexCheck(int index) 
		if(index < 0 || index >= size) 
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		
	
	
	/**
	 * @Description 获取index位置对应的节点
	 * @return
	 */
	private Node<E> getNode(int index) 
		indexCheck(index);
		Node<E> temp = first;
		for(int i = 0;i < index;i++) 
			temp = temp.next;
		
		return temp;
	

	/**
	 * @Description 是否为空
	 * @return
	 */
	public boolean isEmpty() 
		return size == 0;
	

	/**
	 * @Description 元素的数量
	 * @return size
	 */
	public int size() 
		return size;
	

	/**
	 * @Description 往index位置添加元素
	 * @param index
	 * @param element
	 * @return
	 */
	public void add(int index, E element) 
		if(index < 0 || index > size) 
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		
		if (index == 0) 
			first = new Node<>(element, first);
		 else 
			Node<E> prev = getNode(index - 1);
			prev.next = new Node<>(element, prev.next);
		
		size++;
	

	/**
	 * @Description 添加元素到最后面
	 * @param element
	 */
	public void add(E element) 
		add(size, element);
	

	/**
	 * @Description 删除index位置对应的元素
	 * @param index
	 * @return oldEle
	 */
	public E remove(int index) 
		indexCheck(index);
		Node<E> node = first;
		if (index == 0) 
			first = first.next;
		 else 
			Node<E> prev = getNode(index - 1);
			node = prev.next;
			prev.next = node.next;
		
		size--;
		return node.element;
	

	/**
	 * @Description 设置index位置的元素
	 * @param index
	 * @param element
	 * @return
	 */
	public E set(int index, E element) 
		Node<E> node = getNode(index);
		E oldElement = node.element;
		node.element = element;
		return oldElement;
	

	/**
	 * @Description 返回index位置对应的元素
	 * @param index
	 * @return
	 */
	public E get(int index) 
		return getNode(index).element;
	

	/**
	 * @Description 查看元素的位置
	 * @param element
	 * @return
	 */
	public int indexOf(E element) 
		if (element == null) 
			Node<E> node = first;
			for (int i = 0; i < size; i++) 
				if (node.element == null) return i;
				
				node = node.next;
			
		 else 
			Node<E> node = first;
			for (int i = 0; i < size; i++) 
				if (element.equals(node.element)) return i;
				
				node = node.next;
			
		
		return ELEMENT_NOT_FOUND;
	

	/**
	 * @Description 是否包含某个元素
	 * @param element
	 * @return
	 */
	public boolean contains(E element) 
		return indexOf(element) != ELEMENT_NOT_FOUND;
	

	/**
	 * @Description 清除所有元素
	 */
	public void clear() 
		size = 0;
		first = null;
	
	
	@Override
	public String toString() 
		Node<E> temp = first;
		StringBuilder str = new StringBuilder();
		for(int i = 0;i < size;i++) 
			if(i != 0) 
				str.append(",");
			
			str.append(temp.element);
			temp = temp.next;
		
		return "size=" + size + ", [" + str + "]";
	
	
	/**
	 * @Description 节点内部类
	 */
	private static class Node<E> 
		E element;
		Node<E> next;
		
		public Node(E element, Node<E> next) 
			this.element = element;
			this.next = next;
		

		@Override
		public String toString() 
			return element + "";
		
	
	

3. 双向链表LinkedList

3.1 单向链表缺点

单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。

单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除节点,总是要先找到待删除节点的前一个节点。

3.2 实现

public class LinkedList<E> implements List<E>
	/**
	 *	元素的数量
	 */
	private int size;
	
	/**
	 *  指向第一个节点的指针
	 */
	private Node<E> first;

	/**
	 *  指向最后一个节点的指针
	 */
	private Node<E> last;
	
	/**
	 * 	元素未找到返回的下标
	 */
	private static final int ELEMENT_NOT_FOUND = -1;

	/**
	 * @Description 判断下标是否越界
	 */
	private void indexCheck(int index) 
		if(index < 0 || index >= size) 
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		
	
	
	/**
	 * @Description 获取index位置对应的节点
	 * @return
	 */
	private Node<E> getNode(int index) 
		indexCheck(index);
		if(index < (size << 1)) 
			Node<E> temp = first;
			for(int i = 0;i < index;i++) 
				temp = temp.next;
			
			return temp;
		 else 
			Node<E> temp = last;
			for(int i = size - 1;i > index;i--) 
				temp = temp.prev;
			
			return temp;
		
	

	/**
	 * @Description 是否为空
	 * @return
	 */
	public boolean isEmpty() 
		return size == 0;
	

	/**
	 * @Description 元素的数量
	 * @return size
	 */
	public int size() 
		return size;
	

	/**
	 * @Description 往index位置添加元素
	 * @param index
	 * @param element
	 * @return
	 */
	public void add(int index, E element) 
		if(index < 0 || index > size) 
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		
		if(index == size)  //往最后面添加元素时
			Node<E> oldLast = last;
			last = new Node<E>(oldLast,element,null);
			if(oldLast == null)  //链表添加第一个元素时
				first = last;
			 else 
				oldLast.next = last;
			
		 else 
			Node<E> next = getNode(index);
			Node<E> prev = next.prev;
			Node<E> node = new Node<E>(prev,element,next);
			next.prev = node;
			if(prev == null)  //=>index == 0时
				first = node;
			 else 
				prev.next = node;
			
		
		
		size++;
	

	/**
	 * @Description 添加元素到最后面
	 * @param element
	 */
	public void add(E element) 
		add(size, element);
	

	/**
	 * @Description 删除index位置对应的元素
	 * @param index
	 * @return oldEle
	 */
	public E remove(int index) 
		indexCheck(index);
		Node<E> node = getNode(index);
		Node<E> prev = node.prev;
		Node<E> next = node.next;
		if(prev == null)  //index == 0
			first = next;
		 else 
			prev.next = next;
		
		if(next == null)  //index == size - 1
			last = prev;
		 else 
			next.prev = prev;
		
		size--;
		return node.element;
	

	/**
	 * @Description 设置index位置的元素
	 * @param index
	 * @param element
	 * @return
	 */
	public E set(int index, E element) 
		Node<E> node = getNode(index);
		E oldElement = node.element;
		node.element = element;
		return oldElement;
	

	/**
	 * @Description 返回index位置对应的元素
	 * @param index
	 * @return
	 */
	public E get(int index) 
		return getNode(index).element;
	

	/**
	 * @Description 查看元素的位置
	 * @param element
	 * @return
	 */
	public int indexOf(E element) 
		if (element == null) 
			Node<E> node = first;
			for (int i = 0; i < size; i++) 
				if (node.element == null) return i;
				
				node = node.next;
			
		 else 
			Node<E> node = first;
			for (int i = 0; i < size; i++) 
				if (element.equals(node.element)) return i;
				
				node = node.next;
			
		
		return ELEMENT_NOT_FOUND;
	

	/**
	 * @Description 是否包含某个元素
	 * @param element
	 * @return
	 */
	public boolean contains(E element) 
		return indexOf(element) != ELEMENT_NOT_FOUND;
	

	/**
	 * @Description 清除所有元素
	 */
	public void clear() 
		size = 0;
		first = null;
		last = null;
		/*
		 * gc root对象:① 被栈指针指向的对象,如new LinkedList()
		 * 
		 * => 只要断掉first和last,当前链表不被gc root对象指向就
		 *    会被回收。
		 */
	
	
	@Override
	public String toString() 
		Node<E> temp = first;
		StringBuilder str = new StringBuilder();
		for(int i = 0;i < size;i++) 
			if(i != 0) 
				str.append(",");
			
			str.append(temp);
			temp = temp.next;
		
		return "size=" + size + ", [" + str + "]";
	
	
	/**
	 * @Description 节点内部类
	 */
	private static class Node<E> 
		Node<E> prev;
		E element;
		Node<E> next;
		
		public Node(Node<E> prev,E element, Node<E> next) 
			this.prev = prev;
			this.element = element;
			this.next = next;
		

		@Override
		public String toString() 
			StringBuilder str = new StringBuilder();
			if(prev != null) 
				str.append(prev.element);
			
			str.append("_").append(element).append("_");
			if(next != null) 
				str.append(next.element);
			
			return str + "";
		
	
	

3.3 ArrayList与LinkedList对比

ArrayList开辟,销毁内存空间的次数相对较少,但可能造成内存空间浪费(缩容解决)。LinkedList开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费。

如果频繁在尾部进行添加,删除操作,动态数组与双向链表均可选择。

如果频繁在头部进行添加,删除操作,建议选择使用双向链表。

如果有频繁的(在任意位置)添加,删除操作,建议选择双向链表。

如果有频繁的查询操作(随机访问操作),建议选择动态数组。

是否有了双向链表,单向链表就没任何用处了? => 并非如此,在哈希表的设计中就用到了单链表。

4. 循环链表LinkedList

4.1 单向循环链表

注意:单向循环链表相对于单链表(SingleLinkedList)只需修改添加和删除。

/**
 * @Description 往index位置添加元素
 * @param index
 * @param element
 * @return
 */
public void add(int index, E element) 
	if(index < 0 || index > size) 
		throw new IndexOutOfBoundsException("Index:" 
				+ index + ",Size:" + size);
	
	if (index == 0) 
		Node<E> newFirst = new Node<>(element, first);
		//拿到最后一个节点
		Node<E> last = (size == 0) ? newFirst : getNode(size - 1);
		last.next = newFirst;
		first = newFirst;
	 else 
		Node<E> prev = getNode(index - 1);
		prev.next = new Node<>(element, prev.next);
	
	size++;

/**
 * @Description 删除index位置对应的元素
 * @param index
 * @return oldEle
 */
public E remove(int index) 
	indexCheck(index);
	Node<E> node = first;
	if (index == 0) 
		if(size == 1) 
			first = null;
		 else 
			//拿到最后一个节点,注意一定要在改变first之前
			Node<E> last = getNode(size - 1);
			first = first.next;
			last.next = first;
		
	 else 
		Node<E> prev = getNode(index - 1);
		node = prev.next;
		prev.next = node.next;
	
	size--;
	return node.element;

4.2 双向循环链表

注意:双向循环链表相对于双向链表(LinkedList)只用修改添加和删除。

/**
 * @Description 往index位置添加元素
 * @param index
 * @param element
 * @return
 */
public void add(int index, E element) 
	if(index < 0 || index > size) 
		throw new IndexOutOfBoundsException("Index:" 
				+ index + ",Size:" + size);
	
	if(index == size)  //往最后面添加元素时
		Node<E> oldLast = last;
		last = new Node<E>(oldLast,element,first);
		if(oldLast == null)  //链表添加第一个元素时
			first = last;
			first.next = first;
			first.prev = first;
		 else 
			oldLast.next = last;
			first.prev = last;
		
	 else 
		Node<E> next = getNode(index);
		Node<E> prev = next.prev;
		Node<E> node = new Node<E>(prev,element,next);
		next.prev = node;
		prev.next = node;
		if(index == 0)  //=>index == 0时
			first = node;
		
	
	size++;


/**
 * @Description 删除index位置对应的元素
 * @param index
 * @return node.element
 */
public E remove(int index) 
	indexCheck(index);
	Node<E> node = first;
	if(size == 1) 
		first = null;
		last = null;
	 else 
		node = getNode(index);
		Node<E> prev = node.prev;
		Node<E> next = node.next;
		prev.next = next;
		next.prev = prev;
		if(index == 0)  //index == 0
			first = next;
		 
		if(index == size - 1)  //index == size - 1
			last = prev;
		 
	
	size--;
	return node.element;

4.3 约瑟夫问题 (单向循环链表的应用)

约瑟夫问题:设编号为1,2,3...n的n个人围成一圈,约定编号为 k (1 <= k <= n)的人从1开始报数,数到m的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依此类推,直到所有人出列为止,由此产生一个出列编号的序列。

注意:约瑟夫问题也可以用其它数据结构解决,不一定要用循环链表,但是循环链表解决此问题很简单。

使用循环链表解决约瑟夫问题

为了发挥循环链表的最大威力,可对CircleLinkedList做如下改进:

public class LinkedListTest 	
	@Test
	public void test1() 
		CircleLinkedListForJosephus<Integer> list 
			= new CircleLinkedListForJosephus<Integer>();
		for(int i = 1; i <= 8;i++) 
			list.add(i);
		
		//current指向头节点
		list.reset();
		while(!list.isEmpty()) 
			list.next();
			list.next();
			System.out.print(list.remove() + " ");//数了三次后删除
			//3 6 1 5 2 8 4 7 
		
	


class CircleLinkedListForJosephus<E> implements List<E>
	/**
	 *	元素的数量
	 */
	private int size;
	
	/**
	 *  指向第一个节点的指针
	 */
	private Node<E> first;

	/**
	 *  指向最后一个节点的指针
	 */
	private Node<E> last;
	
	/**
	 * 用于指向某个节点的指针
	 */
	private Node<E> current;
	
	/**
	 * 	元素未找到返回的下标
	 */
	private static final int ELEMENT_NOT_FOUND = -1;

	/**
	 * @Description 判断下标是否越界
	 */
	private void indexCheck(int index) 
		if(index < 0 || index >= size) 
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		
	
	
	/**
	 * @Description 获取index位置对应的节点
	 * @return
	 */
	private Node<E> getNode(int index) 
		indexCheck(index);
		if(index < (size << 1)) 
			Node<E> temp = first;
			for(int i = 0;i < index;i++) 
				temp = temp.next;
			
			return temp;
		 else 
			Node<E> temp = last;
			for(int i = size - 1;i > index;i--) 
				temp = temp.prev;
			
			return temp;
		
	

	/**
	 * @Description 让current指向头节点
	 */
	public void reset() 
		current = first;
	
	
	/**
	 * @Description 让current后移一步
	 * @return
	 */
	public E next() 
		if(current == null) return null;
		current = current.next;
		return current.element;
	
	
	/**
	 * @Description 删除current所指向的节点,并将current下移
	 * @return
	 */
	public E remove() 
		if(current == null) return null;
		Node<E> next = current.next;
		int index = indexOf(current.element);
		E element = remove(index);
		if(size == 0) 
			current = null;
		 else 
			current = next;
		
		return element;
	
	
	/**
	 * @Description 是否为空
	 * @return
	 */
	public boolean isEmpty() 
		return size == 0;
	

	/**
	 * @Description 元素的数量
	 * @return size
	 */
	public int size() 
		return size;
	

	/**
	 * @Description 往index位置添加元素
	 * @param index
	 * @param element
	 * @return
	 */
	public void add(int index, E element) 
		if(index < 0 || index > size) 
			throw new IndexOutOfBoundsException("Index:" 
					+ index + ",Size:" + size);
		
		if(index == size)  //往最后面添加元素时
			Node<E> oldLast = last;
			last = new Node<E>(oldLast,element,first);
			if(oldLast == null)  //链表添加第一个元素时
				first = last;
				first.next = first;
				first.prev = first;
			 else 
				oldLast.next = last;
				first.prev = last;
			
		 else 
			Node<E> next = getNode(index);
			Node<E> prev = next.prev;
			Node<E> node = new Node<E>(prev,element,next);
			next.prev = node;
			prev.next = node;
			if(index == 0)  //=>index == 0时
				first = node;
			
		
		size++;
	

	/**
	 * @Description 添加元素到最后面
	 * @param element
	 */
	public void add(E element) 
		add(size, element);
	

	/**
	 * @Description 删除index位置对应的元素
	 * @param index
	 * @return node.element
	 */
	public E remove(int index) 
		indexCheck(index);
		Node<E> node = first;
		if(size == 1) 
			first = null;
			last = null;
		 else 
			node = getNode(index);
			Node<E> prev = node.prev;
			Node<E> next = node.next;
			prev.next = next;
			next.prev = prev;
			if(index == 0)  //index == 0
				first = next;
			 
			if(index == size - 1)  //index == size - 1
				last = prev;
			 
		
		size--;
		return node.element;
	

	/**
	 * @Description 设置index位置的元素
	 * @param index
	 * @param element
	 * @return
	 */
	public E set(int index, E element) 
		Node<E> node = getNode(index);
		E oldElement = node.element;
		node.element = element;
		return oldElement;
	

	/**
	 * @Description 返回index位置对应的元素
	 * @param index
	 * @return
	 */
	public E get(int index) 
		return getNode(index).element;
	

	/**
	 * @Description 查看元素的位置
	 * @param element
	 * @return
	 */
	public int indexOf(E element) 
		if (element == null) 
			Node<E> node = first;
			for (int i = 0; i < size; i++) 
				if (node.element == null) return i;
				
				node = node.next;
			
		 else 
			Node<E> node = first;
			for (int i = 0; i < size; i++) 
				if (element.equals(node.element)) return i;
				
				node = node.next;
			
		
		return ELEMENT_NOT_FOUND;
	

	/**
	 * @Description 是否包含某个元素
	 * @param element
	 * @return
	 */
	public boolean contains(E element) 
		return indexOf(element) != ELEMENT_NOT_FOUND;
	

	/**
	 * @Description 清除所有元素
	 */
	public void clear() 
		size = 0;
		first = null;
		last = null;
		/*
		 * gc root对象:① 被栈指针指向的对象,如new LinkedList()
		 * 
		 * => 只要断掉first和last,当前链表不被gc root对象指向就
		 *    会被回收。
		 */
	
	
	@Override
	public String toString() 
		Node<E> temp = first;
		StringBuilder str = new StringBuilder();
		for(int i = 0;i < size;i++) 
			if(i != 0) 
				str.append(",");
			
			str.append(temp);
			temp = temp.next;
		
		return "size=" + size + ", [" + str + "]";
	
	
	/**
	 * @Description 节点内部类
	 */
	private static class Node<E> 
		Node<E> prev;
		E element;
		Node<E> next;
		
		public Node(Node<E> prev,E element, Node<E> next) 
			this.prev = prev;
			this.element = element;
			this.next = next;
		

		@Override
		public String toString() 
			StringBuilder str = new StringBuilder();
			if(prev != null) 
				str.append(prev.element);
			
			str.append("_").append(element).append("_");
			if(next != null) 
				str.append(next.element);
			
			return str + "";
		
	
	

5. 栈(stack)

5.1 理解

栈是一个先入后出(FILO => First In Last Out)的有序列表。往栈中添加元素的操作,一般叫做入栈(push)。从栈中移除元素的操作,一般叫做 出栈(pop),注意只能移除栈顶元素,也叫做弹出栈顶元素。

栈是限制线性表中元素的插入和删除 只能在线性表的同一端 进行的一种特殊线性表。允许插入和删除的一端为变化的一端,称为 栈顶(Top),另一端为固定的一端,称为 栈底(Bottom)

出栈(pop)和入栈(push)的概念如下:

注意:这里说的“栈”与内存中的“栈空间”是两个不同的概念。

5.2 栈的应用场景

  • 子程序的调用:在跳往子程序前,会先将下一个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。

  • 处理递归调用:和子程序的调用类似,只是除了存储下一个指令的地址外,也将参数,区域变量等数据存入堆栈中。

  • 表达式的转换与求值(实际解决):如中缀表达式转后缀表达式

  • 二叉树的遍历

  • 图形的深度优先(depth-first)搜索法

5.3 ArrayList模拟栈

public class StackTest 
	@Test
	public void test1() 
		ArrayListStack<Integer> stack = new ArrayListStack<Integer>();
		stack.push(11);
		stack.push(22);
		stack.push(33);
		stack.push(44);
		System.out.println(stack.peek());//44
           stack.list();
		while(!stack.isEmpty()) 
			System.out.print(stack.pop() + " ");
			//44 33 22 11 
		
		System.out.println(stack.isEmpty());
		
	


class ArrayListStack<E> 
	private List<E> list = new ArrayList<E>();
	
	//栈的长度
	public int size() 
		return list.size();
	
	
	// 判断栈空
	public boolean isEmpty() 
		return list.isEmpty();
	

	// 入栈
	public void push(E element) 
		list.add(element);
	

	// 出栈
	public E pop() 
		return list.remove(list.size() - 1);
	

	// 获取栈顶元素
	public E peek() 
		return list.get(list.size() - 1);
	
 
   //遍历栈
	public void list() 
		if(isEmpty()) 
			System.out.println("栈空,无数据!");
			return;
		
		for (int i = list.size() - 1; i >= 0; i--) 
			System.out.println("stack[" + i + "] = " + list.get(i));
		
	

5.4 LinkedList模拟栈

//只需要将以上代码的
private List<E> list = new ArrayList<E>();
//改为
private List<E> list = new LinkedList<E>();

5.5 栈的应用-综合计算器(自定义优先级)

即使用栈计算一个中缀表达式的结果

public class Calculator 
	public static void main(String[] args) 
		String expression = "7*2*2-5+1+4/2";
		calculator(expression);
            //表达式 7*2*2-5+1+4/2 的结果为:26
	
	
	public static void calculator(String expression) 
		ArrayListStack2<Integer> numStack = new ArrayListStack2<>();
		ArrayListStack2<Integer> operStack = new ArrayListStack2<>();
		int index = 0;//用于扫描
		int num1 = 0;
		int num2 = 0;
		int oper = 0;
		int res = 0;
		char ch = \' \';//将每次扫描得到的字符保存到ch
		String keepNum = "";//用于拼接多位数
		//开始循环扫描expression
		while(true) 
			ch = expression.substring(index, index + 1).charAt(0);
			if(operStack.isOper(ch)) 
				if(!operStack.isEmpty()) 
					if(operStack.priority(ch) <= 
							operStack.priority(operStack.peek())) 
						num1 = numStack.pop();
						num2 = numStack.pop();
						oper = operStack.pop();
						res = numStack.cal(num1, num2, oper);
						//将运算结果入数栈
						numStack.push(res);
						//将操作符入符号栈
						operStack.push(ch + 0);
					 else 
						//当前操作符的优先级大于栈中的操作符优先级,直接入符号栈
						operStack.push(ch + 0);
					
				 else 
					//符号栈为空就直接入符号栈
					operStack.push(ch + 0);
				
			 else 
				//如果是数就直接入数栈
				//numStack.push(ch - 48);//\'1\' => 1  (只能处理一位数)
				//能够处理多位数的思路:
					//当处理数时,需要向expression表达式的index后再看一位,
					//如果是数就拼接并继续扫描,是符号才入栈
				keepNum += ch;
				if(index == expression.length() - 1) 
					//如果ch已经是expression的最后一位,就直接入栈
					numStack.push(Integer.parseInt(keepNum));
				 else 
					if(operStack.isOper(expression.substring(index + 1, index + 2)
							.charAt(0))) 
						numStack.push(Integer.parseInt(keepNum));
						//将keepNum清空
						keepNum = "";
					
				
			
			//使index + 1,并判断是否扫描到expression的最后
			index++;
			if(index >= expression.length()) 
				break;
			
		
		
		//当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号并运算
		while(true) 
			if(operStack.isEmpty()) 
				break;
			
			num1 = numStack.pop();
			num2 = numStack.pop();
			oper = operStack.pop();
			res = numStack.cal(num1, num2, oper);
			numStack.push(res);
		
		//将数栈的最后数pop出,得到结果
		int res2 = numStack.pop();
		System.out.println("表达式 " + expression 
				+ " 的结果为:" + res2);
	


//数组模拟栈,需要扩展一些功能
class ArrayListStack2<E> 
	private List<E> list = new ArrayList<E>();
	
	//栈的长度
	public int size() 
		return list.size();
	
	
	// 判断栈空
	public boolean isEmpty() 
		return list.isEmpty();
	

	// 入栈
	public void push(E element) 
		list.add(element);
	

	// 出栈
	public E pop() 
		return list.remove(list.size() - 1);
	

	// 获取栈顶元素
	public E peek() 
		return list.get(list.size() - 1);
	
	
	//遍历栈
	public void list() 
		if(isEmpty()) 
			System.out.println("栈空,无数据!");
			return;
		
		for (int i = list.size() - 1; i >= 0; i--) 
			System.out.println("stack[" + i + "] = " + list.get(i));
		
	
	
	//返回运算符的自定义优先级(假定优先级使用数字表示)
	public int priority(int oper) 
		if(oper == \'*\' || oper == \'/\') 
			return 1;
		 else if(oper == \'+\' || oper == \'-\') 
			return 0;
		 else 
			return -1;//假定目前的表达式只有+-*/
		
	
	
	//判断当前字符是否是一个运算符
	public boolean isOper(char val) 
		return val == \'+\' || val == \'-\' || val == \'*\' || val == \'/\';
	
	
	//计算两个操作数的方法
	public int cal(int num1,int num2,int oper) 
		int res = 0;
		switch(oper) 
		case \'+\':
			res = num2 + num1;
			break;
		case \'-\':
			res = num2 - num1;
			break;
		case \'*\':
			res = num2 * num1;
			break;
		case \'/\':
			res = num2 / num1;
			break;
		default:
			break;
		
		return res;
	

5.6 栈的应用-逆波兰计算器

逆波兰表达式(前缀表达式)

前缀表达式的运算符位于操作数之前。

前缀表达式的计算机求值:从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果

//举例:(3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下:
//① 从右至左扫描,将6、5、4、3压入堆栈
//② 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈
//③ 接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈
//④ 最后是-运算符,计算出35-6的值,即29,由此得出最终结果

中缀表达式

中缀表达式就是常见的运算表达式,如(3+4)×5-6

中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(上述案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式)

后缀表达式

与前缀表达式相似,只是运算符位于操作数之后

后缀表达式的计算机求值

从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果

//例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:
//① 从左至右扫描,将3和4压入堆栈;
//② 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
//③ 将5入栈;
//④ 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
//⑤ 将6入栈;
//⑥ 最后是-运算符,计算出35-6的值,即29,由此得出最终结果

中缀表达式转换为后缀表达式

后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,需要将中缀表达式转成后缀表达式

具体步骤:

//1.初始化两个栈:运算符栈s1和储存中间结果的栈s2;
//2.从左至右扫描中缀表达式;
//3.遇到操作数时,将其压s2;
//4.遇到运算符时,比较其与s1栈顶运算符的优先级:
//    ① 如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
//    ② 否则,若优先级比栈顶运算符的高,也将运算符压入s1;
//    ③ 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶
//      运算符相比较; 
//5.遇到括号时:
//   ① 如果是左括号“(”,则直接压入s1
//   ② 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号
//     为止,此时将这一对括号丢弃
//6.重复步骤2至5,直到表达式的最右边
//7.将s1中剩余的运算符依次弹出并压入s2
//8.依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式

使用栈实现逆波兰计算器(计算整数)

public class ReversePolishCalculate 
	//直接输入一个后缀表达式计算结果
	@Test
	public void test1() 
		// 定义一个逆波兰表达式
		// 为了方便,逆波兰表达式的数字和符号使用空格隔开
		// (30+4)*5-6 => 30 4 + 5 * 6 - => 29
		// 4*5-8+60+8/2 => 4 5 * 8 - 60 + 8 2 / + => 76
		String suffixExpression = "4 5 * 8 - 60 + 8 2 / +";
		// 思路:
		//   ① 先将 "3 4 + 5 * 6 -" 放入ArrayList中
		//   ② 将ArrayList 传递给一个方法,遍历ArrayList配合栈完成计算

		List<String> list = getListString(suffixExpression);
		System.out.println(list);
		int res = calculate(list);
		System.out.println("计算的结果是 " + res);
	
	
	// 完成将一个中缀表达式转为后缀表达式的功能并计算结果
	@Test
	public void test2() 
		// 思路:
		// 	 ① 直接对str操作不方便,因此先将中缀表达式字符串转换为List
		//   ② 中缀表达式对应的list => 后缀表达式对应的list
		String expression = "1+((2+3)*4)-5";
		List<String> list = toInfixExpressionList(expression);
		System.out.println(list);
		// [1, +, (, (, 2, +, 3, ), *, 4, ), -, 5]
		List<String> list2 = parseSuffixExpressionList(list);
		System.out.println(list2);
		//[1, 2, 3, +, 4, *, +, 5, -]
		System.out.println("expression的计算结果为:" + calculate(list2));//16
	

	// 将一个中缀表达式转换成对应的List
	public static List<String> toInfixExpressionList(String s) 
		List<String> ls = new ArrayList<String>();
		int i = 0;// 用于遍历中缀表达式字符串s的指针
		String str;// 对多位数的拼接
		char c;// 每遍历一个字符,就放入c
		do 
			// \'0\' => [48] \'9\' => [57]
			if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) 
				// 如果c是一个非数字,就加入ls中
				ls.add("" + c);
				i++;// i需要后移
			 else 
				// 如果是一个数字,需要考虑多位数
				str = "";// 先将str置空
				while (i < s.length() && (c = s.charAt(i)) > 48 && (c = s.charAt(i)) <= 57) 
					str += c;// 拼接
					i++;
				
				ls.add(str);
			
		 while (i < s.length());
		return ls;
	

	//中缀表达式对应的list => 后缀表达式对应的list
	public static List<String> parseSuffixExpressionList(List<String> ls) 
		Stack<String> s1 = new Stack<String>();// 符号栈
		// 注意:因为s2这个栈在整个转换过程中没有pop操作且后面要逆序输出,
		//      很麻烦,所以用ArrayList代替。
		// Stack<String> s2 = new Stack<String>();//存储中间结果的栈
		List<String> s2 = new ArrayList<String>();// 存储中间结果的List
		// 遍历ls
		for (String item : ls) 
			// 如果是一个数字,入s2
			if (item.matches("\\\\d+")) 
				s2.add(item);
			 else if (item.equals("(")) 
				s1.push(item);
			 else if(item.equals(")")) 
				while(!s1.peek().equals("(")) 
					s2.add(s1.pop());
				
				s1.pop();//将"("弹出s1栈
			 else 
				//思路:当item的优先级小于等于s1栈顶运算符时,将s1栈顶的运算符弹出
				//      并加入s2中,再次与s1中新的栈顶运算符比较优先级。
				//注意:我们需要一个比较运算符优先级高低的方法
				while(s1.size() != 0 && 
						Operation.getValue(s1.peek()) 
						>= Operation.getValue(item)) 
					s2.add(s1.pop());
				
				//还需要将item压入栈中
				s1.push(item);
			
		
		//将s1中剩余的运算符依次弹出并加入s2
		while(s1.size() != 0) 
			s2.add(s1.pop());
		
		return s2;//注意因为是存放到List中的,所以按顺序输出即可
	

	// 将一个逆波兰表达式的数据和运算符放入ArrayList中
	public static List<String> getListString(String suffixExpression) 
		// 将suffixExpression分隔
		String[] split = suffixExpression.split(" ");
		ArrayList<String> list = new ArrayList<String>();
		for (String ele : split) 
			list.add(ele);
		
		return list;
	

	// 完成对逆波兰表达式的运算
	public static int calculate(List<String> ls) 
		Stack<String> stack = new Stack<String>();
		for (String item : ls) 
			// 使用正则表达式取出数
			if (item.matches("\\\\d+")) // 匹配的是多位数
				stack.push(item);
			 else 
				// pop出两个数运算,结果入栈
				int num2 = Integer.parseInt(stack.pop());
				int num1 = Integer.parseInt(stack.pop());
				int res = 0;
				if (item.equals("+")) 
					res = num1 + num2;
				 else if (item.equals("-")) 
					res = num1 - num2;
				 else if (item.equals("*")) 
					res = num1 * num2;
				 else if (item.equals("/")) 
					res = num1 / num2;
				 else 
					throw new RuntimeException("运算符有误!");
				
				// 把res入栈
				stack.push("" + res);
			
		
		// 最后留在stack中数据就是运算结果
		return Integer.parseInt(stack.pop());
	


//返回一个运算符对应的优先级的类
class Operation 
	private static int ADD = 1;
	private static int SUB = 1;
	private static int MUL = 2;
	private static int DIV = 2;
	
	public static int getValue(String operation) 
		int result = 0;
		switch (operation) 
		case "+":
			result = ADD;
			break;
		case "-":
			result = SUB;
			break;
		case "*":
			result = MUL;
			break;
		case "/":
			result = DIV;
			break;
		default:
			System.out.println("不存在该运算符!");
			break;
		
		return result;
	

6.队列(queue)

6.1 理解