Java面试 Java八股文 数据结构【持续更新】

基础篇

基础算法

二分查找

二分查找顾名思义一半一半查找,具体要怎么一半呢,下面画一张图进行详解

在这里插入图片描述

二分查找实现

我们都知道数组的中间位置为0下标加上最大下标位置之和除以2可以得到中间位置

int l = 0;
int r = arr.length - 1;
int middle = (l + r) / 2 // 中间	

那么拿中间下标开始寻找目标值,如果比他大那么目标在数组中间位置靠右反之靠左

int target = 20;
if(arr[middle] == target){
	return middle;
}else if(arr[middle] > target){
	l = middle + 1;
}else if(arr[middle] < target){
	r = middle - 1;
}
代码实现
		int l = 0;
        int r = arr.length - 1;
        while (l <= r) {
            int middle = (l + r) >>> 1;
            if (arr[middle] == target) {
                System.out.println(arr[middle]);
                return middle;
            } else if (arr[middle] > target) {
                r = middle - 1;
            } else if (arr[middle] < target) {
                l = middle + 1;
            }
        }
        return -1;

二分查找前提被查找的数据必须是排序好的并且以升序排序

冒泡排序

冒泡排序是很经典的排序方法,也是很基础的算法,冒泡排序算法精髓就在相邻比大小,大的交换,先看一下下图

在这里插入图片描述

代码实现

从上图可以看出相邻的较大数都互相交换位置,接下来只需要不断循环这个过程了

    for (int i = 0; i < arr.length - 1; i++) {
                if (arr[i] > arr[i + 1]) {
                    swap(arr, i, i + 1);
                }
            }
  // 交换函数      
  public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    // 循环这个交换过程即可  
    public static void sort(int[] arr) {
        for (int j = 0; j < arr.length; j++) {
            for (int i = 0; i < arr.length - 1; i++) {
                if (arr[i] > arr[i + 1]) {
                    swap(arr, i, i + 1);
                }
            }
        }
    }

但是这样一直循环,如果循环还没有结束数组就已经排序好了,需要优化一下这个函数

交换次数:1,数组:[1, 0, 5, 20, 18, 30, 7, 9, 10, 4, 6, 50]
交换次数:2,数组:[0, 1, 5, 18, 20, 7, 9, 10, 4, 6, 30, 50]
交换次数:3,数组:[0, 1, 5, 18, 7, 9, 10, 4, 6, 20, 30, 50]
交换次数:4,数组:[0, 1, 5, 7, 9, 10, 4, 6, 18, 20, 30, 50]
交换次数:5,数组:[0, 1, 5, 7, 9, 4, 6, 10, 18, 20, 30, 50]
交换次数:6,数组:[0, 1, 5, 7, 4, 6, 9, 10, 18, 20, 30, 50]
交换次数:7,数组:[0, 1, 5, 4, 6, 7, 9, 10, 18, 20, 30, 50]
交换次数:8,数组:[0, 1, 4, 5, 6, 7, 9, 10, 18, 20, 30, 50]
交换次数:9,数组:[0, 1, 4, 5, 6, 7, 9, 10, 18, 20, 30, 50]
交换次数:10,数组:[0, 1, 4, 5, 6, 7, 9, 10, 18, 20, 30, 50]
交换次数:11,数组:[0, 1, 4, 5, 6, 7, 9, 10, 18, 20, 30, 50]
交换次数:12,数组:[0, 1, 4, 5, 6, 7, 9, 10, 18, 20, 30, 50]
通过打印循环次数,我们可以看到在第8次就排序好了,但是多循环了4

优化思路很简单,如果我们到了排序好了循环次数,那么就没有数字需要交换,那么只需要判断有没有交换就可以啦

public static void sort(int[] arr) {
        for (int j = 0; j < arr.length; j++) {
            // 这里判断是否交换了 
            boolean isSwap = false;
            for (int i = 0; i < arr.length - 1; i++) {
                if (arr[i] > arr[i + 1]) {
                    swap(arr, i, i + 1);
                    // 发生交换就会将状态变为true
                    isSwap = true;
                }
            }
            // 如果没有发生交换,就说明结束了
            if (!isSwap) {
                break;
            }
            System.out.println("交换次数:" + (j + 1) + ",数组:" + Arrays.toString(arr));
        }
    }
console:
交换次数:1,数组:[1, 0, 5, 20, 18, 30, 7, 9, 10, 4, 6, 50]
交换次数:2,数组:[0, 1, 5, 18, 20, 7, 9, 10, 4, 6, 30, 50]
交换次数:3,数组:[0, 1, 5, 18, 7, 9, 10, 4, 6, 20, 30, 50]
交换次数:4,数组:[0, 1, 5, 7, 9, 10, 4, 6, 18, 20, 30, 50]
交换次数:5,数组:[0, 1, 5, 7, 9, 4, 6, 10, 18, 20, 30, 50]
交换次数:6,数组:[0, 1, 5, 7, 4, 6, 9, 10, 18, 20, 30, 50]
交换次数:7,数组:[0, 1, 5, 4, 6, 7, 9, 10, 18, 20, 30, 50]
交换次数:8,数组:[0, 1, 4, 5, 6, 7, 9, 10, 18, 20, 30, 50]

选择排序

顾名思义每次选择一个元素当做已排序的区域,接着继续寻找未排序中合适的元素放到排序好的区域,那么这个合适的数字该怎么寻找呢,请看一下图解

在这里插入图片描述

代码实现
    public static void sort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            int select = i;
            for (int j = select + 1; j < arr.length; j++) {
                if (arr[select] > arr[j]) {
                    select = j;
                }
            }
            swap(arr, select, i);
        }
    }
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

插入排序

插入排序和选择排序都是将已排序区域和未排序区域划分,不断缩小未排序区域达到排序效果

在这里插入图片描述

代码实现
    public static void sort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            // 定义临时变量,这个变量最后要插入到已排序区域
            int temp = arr[i];
            // 定义已排序区域变量
            int j = i - 1;
            while (j >= 0) {
                // 判断temp是否小于当前元素,交换,反之退出循环
                if (temp < arr[j]) {
                    arr[j + 1] = arr[j];
                } else {
                    break;
                }
                j--;
            }
            // 当上述循环退出,j + 1就是temp要插入的位置
            arr[j + 1] = temp;
        }
    }

快速排序

快速排序实现方式有很多种,这里挑了两种较为经典的实现方式作为案例,lomuto法 和 双边循环法

lomuto法

该方法实现较为简单,可以很快理解快速排序,lomuto法又称单边循环法,该方法实现的快速排序以最右边元素作为基准点

在这里插入图片描述

代码实现
	// 对分区不断循环
    public static void quick(int[] arr, int l, int h) {
        if (l >= h) {
            return;
        }
        int index = lomutoPartition(arr, l, h);
        quick(arr, l, index - 1);
        quick(arr, index + 1, h);
    }

    // 分区函数
    public static int lomutoPartition(int[] arr, int l, int h) {
        // 基准点
        int pv = arr[h];
        int i = l;
        for (int j = l; j < h; j++) {
            if (arr[j] < pv) {
                swap(arr, i, j);
                i++;
            }
        }
        swap(arr, i, h);
        return i;
    }
    // 交换函数
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
双边循环法

双边循环一般选择基准点为最左边元素开始

代码实现
    public static int doublePartition(int[] arr, int l, int h) {
        // 基准点
        int pv = arr[l];
        int i = l;
        int j = h;
        while (i < j) {
            while (i < j && arr[j] > pv) {
                j--;
            }
            while (i < j && arr[i] <= pv) {
                i++;
            }
            swap(arr, i, j);
        }
        swap(arr, l, j);
        return j;
    }

基础数据结构

ArrayList

初始容量

我们都知道ArrayList底层是数组实现的,那么数组都是有长度的,我们将从以下几个方面剖析ArrayList容量相关知识

  • 数组

在这里插入图片描述

我们都知道数组是连续的数据结构,数组在java中必须初始化容量才可以使用

  • ArrayList源码

    那么这个数组在ArrayList中是什么样子的呢?

        /**
         * Default initial capacity. 默认初始大小
         */
        private static final int DEFAULT_CAPACITY = 10;
    
        /**
         * Shared empty array instance used for empty instances.
         */
        private static final Object[] EMPTY_ELEMENTDATA = {};
    
        /**
         * Shared empty array instance used for default sized empty instances. We
         * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
         * first element is added.
         */
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 
    
        /**
         * The array buffer into which the elements of the ArrayList are stored.
         * The capacity of the ArrayList is the length of this array buffer. Any
         * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
         * will be expanded to DEFAULT_CAPACITY when the first element is added.
         */
        transient Object[] elementData; // non-private to simplify nested class access
    
        /**
         * The size of the ArrayList (the number of elements it contains).
         *
         * @serial
         */
        private int size;   
    
    	/**
         * Constructs an empty list with the specified initial capacity.
         *
         * @param  initialCapacity  the initial capacity of the list
         * @throws IllegalArgumentException if the specified initial capacity
         *         is negative
         */
        public ArrayList(int initialCapacity) {
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }
    
        /**
         * Constructs an empty list with an initial capacity of ten.
         */
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
    

    这里先看这两个关键构造函数

    1. 有参构造

      解释一下这段代码的意思

       public ArrayList(int initialCapacity) {
          // 判断传入的整数是否大于0 
          if (initialCapacity > 0) {
          		// true则创建一个Object数组并赋值给成员变量elementData
                  this.elementData = new Object[initialCapacity];
              } else if (initialCapacity == 0) {
              	// 如果传入的整数为0则将常量赋值给elementData
                  this.elementData = EMPTY_ELEMENTDATA;
              } else {
              	// 如果整数小于0抛出异常
                  throw new IllegalArgumentException("Illegal Capacity: "+
                                                     initialCapacity);
              }
          }
      

      如果我们创建ArrayList传入了初始容量,并且整数符合构造函数要求,那么就会得到一个自定义容量的数组;

      这里解释一下 EMPTY_ELEMENTDATA 如果传入整数为0,则会创建一个长度为0的数组;

    2. 无参构造

      /**
       * Constructs an empty list with an initial capacity of ten.
       */
      public ArrayList() {
          this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
      }
      

      如果不指定初始容量,ArrayList就会创建默认的空数组;

      那岂不是长度为0,添加元素时就会抛出数组下标越界呢,其实并不会;

      我们先看一下add()方法源码

      /**
           * Appends the specified element to the end of this list.
           *
           * @param e element to be appended to this list
           * @return <tt>true</tt> (as specified by {@link Collection#add})
           */
          public boolean add(E e) {
              ensureCapacityInternal(size + 1);  // Increments modCount!!
              elementData[size++] = e;
              return true;
          }
      

      调用add()时会先调用 ensureCapacityInternal(size + 1) 方法,这个方法是干什么的呢?

      我们继续看一下**ensureCapacityInternal()**方法源码

      private void ensureCapacityInternal(int minCapacity) {
          	// 判断当前数组是否为空数组,这个常量创建了一个空数组
              if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                  // 判断传入的变量和默认大小取较大值,默认大小为10
                  minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
              }
      		// 接着调用该方法
              ensureExplicitCapacity(minCapacity);
          }
      

      其实该方法判断的是数组容量够不够放下元素,考虑扩容数组;

      最后该方法调用了 ensureExplicitCapacity(minCapacity) ,还把较大值传入,我们继续看这个方法的源码;

      private void ensureExplicitCapacity(int minCapacity) {
         
          modCount++;
       	// 判断这个传入的变量 - 当前数组的长度是否大于0
          // overflow-conscious code
          if (minCapacity - elementData.length > 0)
              grow(minCapacity);
      }
      

      大家可能很疑惑modCount是什么?

      源码里对这个成员变量进行了解释:

      此列表已在结构上修改的次数。结构修改是那些改变列表大小,或者以其他方式干扰列表的修改,使得正在进行的迭代可能产生不正确的结果。
      此字段由迭代器和列表迭代器方法返回的迭代器和列表迭代器实现使用。如果此字段的值意外更改,则迭代器 (或列表迭代器) 将引发ConcurrentModificationException,以响应下一个,删除,上一个,设置或添加操作。这提供了快速失败的行为,而不是在迭代过程中面对并发修改时的非确定性行为。
      子类使用此字段是可选的。如果一个子类希望提供快速失败的迭代器 (和列表迭代器),那么它只需要在它的add(int,E) 和remove(int) 方法 (以及它覆盖的任何其他导致列表结构修改的方法) 中增加这个字段。单个调用add(int,E) 或remove(int) 必须在此字段中添加不超过一个,否则迭代器 (和列表迭代器) 将抛出虚假的concurrentmodificationexception。如果实现不希望提供快速失败的迭代器,则可以忽略此字段。

      其实意思很明显,如果我们遍历过程中修改了ArrayList元素内容,就会抛出异常,该变量记录着元素的状态;

      再看源码里又调用了 grow(minCapacity) 将较大值传入后,再继续看该方法源码;

      /**
       * Increases the capacity to ensure that it can hold at least the
       * number of elements specified by the minimum capacity argument.
       *
       * @param minCapacity the desired minimum capacity
       */
      private void grow(int minCapacity) {
          // overflow-conscious code
          // 将当前数组长度赋值给oldCapacity
          int oldCapacity = elementData.length;
          // 将oldCapacity + (oldCapacity / 2) 的值赋值给newCapacity
          int newCapacity = oldCapacity + (oldCapacity >> 1);
          // 判断newCapacity - 传入的较大值是否小于0
          if (newCapacity - minCapacity < 0)
              // 将较大值赋值给newCapacity
              newCapacity = minCapacity;
          // 判断newCapacity - MAX_ARRAY_SIZE是否大于0 
          if (newCapacity - MAX_ARRAY_SIZE > 0)
              // 这里调用了函数返回值赋值给newCapacity
              newCapacity = hugeCapacity(minCapacity);
          // minCapacity is usually close to size, so this is a win:
          // 数组进行扩容
          elementData = Arrays.copyOf(elementData, newCapacity);
      }
      

      MAX_ARRAY_SIZE 常量是什么?

      看一下源码的介绍:

      /**
           * The maximum size of array to allocate.
           * Some VMs reserve some header words in an array.
           * Attempts to allocate larger arrays may result in
           * OutOfMemoryError: Requested array size exceeds VM limit
           */
          private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
      

      经过翻译后:

      • 要分配的数组的最大大小。
      • 一些虚拟机在数组中保留一些标题字。
      • 尝试分配更大的数组可能会导致
      • OutOfMemoryError: 请求的数组大小超过虚拟机限制

      如果满足了调用 hugeCapacity(minCapacity) 条件,我们继续查看该方法源码;

          private static int hugeCapacity(int minCapacity) {
              // 如果较大值小于0 抛出异常,溢出了
              if (minCapacity < 0) // overflow
                  throw new OutOfMemoryError();
              // 如果较大值大于MAX_ARRAY_SIZE(Integer最大值-8) 返回int最大值,否则返回int最大值-8
              return (minCapacity > MAX_ARRAY_SIZE) ?
                  Integer.MAX_VALUE :
                  MAX_ARRAY_SIZE;
          }
      
扩容

从上面源码我们不难看出ArrayList初始容量的两次不同的创建,一次是创建对象时指定容量就会立即创建一个指定大小的数组,另一个是调用add的时候才会创建默认大小为 10 的数组

那么扩容时机是什么时候?

答案是每次调用添加方法的时候,包括add(),addAll()等,上述源码介绍到,调用add()的时候会调用很多方法,来判断是否需要扩容,那么扩容时机就是每次调用添加的时候;

那么每次扩容大小是怎么样的呢?

源码这一段,很清晰的告诉我们扩容大小

int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);

当前数组的长度 + 当前数组长度 / 2

假设使用默认长度 10 那么在添加到第11个元素的时候,判断成立进行扩容;;

int oldCapacity = 10;
int newCapacity = 10 + (10 / 2); // 15

那么下次扩容后为15,这里大家很奇怪,为什么容量为15但是调用 size() 方法还是为 11 呢?

因为ArrayList没有对外方法显示数组大小,而是用 size 成员变量记录数组内的元素个数,并提供对外方法 size() 来显示数组内的元素个数

LinkedList

我们将从以下几方面解析LinkedList

链表

在这里插入图片描述

链表最大的特征就是不需要连续内存,节点指向下一个节点,只和相邻节点有联系;

单向链表:节点一直往前,不支持头插法

双向链表:节点一直往前,可以想头部节点插入节点,成为新的头节点

LinkedList介绍
  • LinkedList底层实现为双向链表,当然支持头插法和尾插法
源码分析
transient int size = 0;

/**
 * Pointer to first node.
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * Pointer to last node.
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;

/**
 * Constructs an empty list.
 */
public LinkedList() {
}

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param  c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

研究源码应该先看成员变量和构造函数

  • transient int size = 0;

    记录链表大小,提供对外方法 size() 显示链表节点个数

  • transient Node first;

    头节点

  • transient Node last;

    尾结点

  • public LinkedList(Collection<? extends E> c) {
    this();
    addAll©;
    }

    如果传入的是一个集合,则调用addAll() 添加所有元素

刚才说过双向链表可以头插和尾插,源码里这两个方法分表为头插和尾插

/**
     * Inserts the specified element at the beginning of this list.
     *
     * @param e the element to add
     */
    public void addFirst(E e) {
        linkFirst(e);
    }

    /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #add}.
     *
     * @param e the element to add
     */
    public void addLast(E e) {
        linkLast(e);
    }

这里我们着重看一下add()和remove()方法实现

    /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }

调用add() 又调用了内部方法 linkLast(e) 我们看一下该方法的源码

    void linkLast(E e) {
        // 将尾结点赋值给新的节点 
        final Node<E> l = last;
        // 创建新的节点,传入元素
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        // 判断尾结点是否为null
        if (l == null)
            // true 则链表大小为0,赋值给头节点
            first = newNode;
        else
            // 否则新节点成为尾结点
            l.next = newNode;
        // 链表大小增加
        size++;
        modCount++;
    }

ArrayList和LinkedList比较

ArrayList
  1. 基于数组,需要连续内存
  2. 随机访问快(指根据下标访问)
  3. 尾部插入,删除性能可以,其他部分插入,删除都会移动数据,因此性能会低
  4. 可以利用cpu缓存,局部性原理
LinkedList
  1. 基于双向链表,无需连续内存
  2. 随机访问慢(要沿着链表遍历)
  3. 头尾插入删除性能高
  4. 占用内容多
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值