# 排序算法的介绍

排序也称排序算法 (Sort Algorithm),排序是将一 组数据,依指定的顺序进行排列 的过程

排序不是数据结构而是算法

排序的分类:

  1. 内部排序:

指将需要处理的所有数据都加载 到内部存储器 (内存) 中进行排序。

  1. 外部排序法:

数据量过大,无法全部加载到内 存中,需要借助外部存储 (文件等) 进行

排序。

  1. 常见的排序算法分类 (见下图):

# 算法的时间复杂度

度量一个程序 (算法) 执行时间的两种方法

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

  2. 事前估算的方法
    通过分析某个算法的时间复杂度来判断哪个算法更优.

# 算法的时间复杂度

时间频度

Ø 基本介绍

时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为 T (n)。[举例说明]

Ø 举例说明 - 基本案例

比如计算 1-100 所有数字之和,我们设计两种算法:

T(n)=n+1;

T(n)=1;

Ø 举例说明 - 忽略常数项

Ø 举例说明 - 忽略低次项

Ø 举例说明 - 忽略系数

时间复杂度

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

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

  3. 计算时间复杂度的方法:

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

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

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

# 冒泡排序

基本介绍

冒泡排序(Bubble Sorting)的基本思想是:通过对待

排序序列从前向后(从下标较小的元素开始), 依次比较

相邻元素的值,若发现逆序则交换,使值较大

的元素逐渐从前移向后部,就象水底下的气泡一样逐渐

向上冒。

因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下

来没有进行过交换,就说明序列有序,因此要在排序过程中设置

一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排序写好后,在进行)

演示冒泡过程的例子 (图解)

冒泡排序应用实例

我们举一个具体的案例来说明冒泡法。我们将五个无序的数:3, 9, -1, 10, -2 使用冒泡排序法将其排成一个从小到大的有序数列。

图解:

代码实现:

先将排序的结构拆分了写,步骤如下:

每次进行排序的时候都需要将长度 - 1 - n 因为排序后范围总是要缩小的否则就会一直来回的排序。

public class BubbleSort01 {
    public static void main(String[] args) {
        int[] arr = {-1,-2,3,1,20,10,9,5};
        int temp = 0;
        for(int i = 0;i < arr.length - 1;i ++){
            if(arr[i] > arr[i+1]){
                temp = arr[i];
                arr[i] = arr[i+1];
                arr[i+1] = temp;
            }
        }
        System.out.println("第一趟排序结果:"+ Arrays.toString(arr));
        for(int i = 0;i < arr.length - 1 - 1;i ++){
            if(arr[i] > arr[i+1]){
                temp = arr[i];
                arr[i] = arr[i+1];
                arr[i+1] = temp;
            }
        }
        System.out.println("第二趟排序结果:"+Arrays.toString(arr));
        for(int i = 0;i < arr.length - 1 - 2;i ++){
            if(arr[i] > arr[i+1]){
                temp = arr[i];
                arr[i] = arr[i+1];
                arr[i+1] = temp;
            }
        }
        System.out.println("第三趟排序结果:"+Arrays.toString(arr));
    }
}

简化代码:将排序的过程结合为嵌套循环来完成冒泡排序:

代码如下:

@SuppressWarnings("all")
public class BubbleSort {
    public static void main(String[] args) {
        int temp = 0;
        int[] arr = {-1,-2,3,1,10,9,5};
        // 标识变量
        boolean flag = false;
        // 冒泡排序 的时间复杂度为 O (n^2)
        for(int i = 0;i < arr.length - 1;i ++){
            for(int j = 0;j < arr.length - i - 1;j ++){
                if(arr[j] > arr[j+1]){
                    // 如果进行过交换则将标识变量赋值为 true
                    flag = true;
                    temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                }
            }
            System.out.println("第"+i+"趟,的排序结果为: "+Arrays.toString(arr));
            // 判断标识变量进行对应的操作
            if(!flag){
                // 在一趟排序中,一次交换都没有发生过
                break;
            }else{
                // 如果发生过交换则进入 else 中,将 flag 重置进行下次判断
                flag = false;
            }
        }
    }
}

# 选择排序

基本介绍

选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。

选择排序思想:

选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:第一次从 arr [0] ~ arr [n-1] 中选取最小值,与 arr [0] 交换,第二次从 arr [1] ~ arr [n-1] 中选取最小值,与 arr [1] 交换,第三次从 arr [2] ~ arr [n-1] 中选取最小值,与 arr [2] 交换,…,第 i 次从 arr [i-1]~arr [n-1] 中选取最小值,与 arr [i-1] 交换,…, 第 n-1 次从 arr [n-2]~arr [n-1] 中选取最小值,与 arr [n-2] 交换,总共通过 n-1 次,得到一个按排序码从小到大排列的有序序列。

选择排序思路分析图 *:


101, 34, 119, 1

代码实现:

public static void selectionSort(int[] arr) {
   /* 判断数组为空或为一个元素的情况,即边界检查 */
   if (arr == null || arr.length < 2) {
      return;
   }
   /* 每次要进行比较的两个数,的前面那个数的下标 */
   for (int i = 0; i < arr.length - 1; i++) { 
      //min 变量保存该趟比较过程中,最小元素所对应的索引,
      // 先假设前面的元素为最小元素
      int minIndex = i;
      /* 每趟比较,将前面的元素与其后的元素逐个比较 */
      for (int j = i + 1; j < arr.length; j++) {
         // 如果后面的元素小,将后面元素的索引极为最小值的索引
         if(arr[j] < arr[minIndex]) {
            minIndex = j;
         }
      }
      // 然后交换此次查找到的最小值和原始的最小值
      swap(arr, i, minIndex);
   }
}
public static void swap(int[] arr, int i, int j) {
   int tmp = arr[i];
   arr[i] = arr[j];
   arr[j] = tmp;
}
  • 选择排序的优化
public class Demo {
    public static void main(String[] args) {
        int[] arr = {-1,-2,3,1,20,10,9,5};
        sort(arr);
    }
    public static void sort(int[] arr){
        int left = 0;
        int right = arr.length - 1;
        while(left < right){
            int max = right;
            int min = left;
            for(int i = left;i <= right;i ++){
                if(arr[max] < arr[i])
                    max = i;
                if(arr[min] > arr[i])
                    min = i;
            }
            // 最大值放在最右端
            int temp = arr[max];
            arr[max] = arr[right];
            arr[right] = temp;
            if(min == right){
                min = max;
            }
            // 最下值放在最左端
            temp = arr[min];
            arr[min] = arr[left];
            arr[left] = temp;
            left ++;
            right --;
        }
        System.out.println(Arrays.toString(arr));
    }
}

# 插入排序

插入排序法介绍

插入式排序属于内部排序法,是对于预排序的元素以插入的方式找寻元素的适当位置,以达到排序的目的。

插入排序法思想:

插入排序(Insertion Sorting)的基本思想是:把 n 个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。

插入排序思路图

代码实现:

public static void main(String[]args){
    int[] ar = {5,3,4,6,2,1};
    // 需要使用 j 循环值来进行位置互换
    int j;
    for(int i = 1;i < ar.length;i ++){
        // 取一个值进行比较
        int tmp = ar[i];
        //j = i - 1 循环 0 等于或到 j 的数值,j 也就是 i-1 的值,判断是否满足交换条件执行则 j-- 
        for(j = i-1;j >= 0 && ar[j] > tmp;j --){
            // 将大的元素赋值到索引前
            ar[j+1] = ar[j];
        }
        // 将元素小的赋值到索引后,因为 j-- j 循环变量在满足条件执行方法体时不会及时执行 j--
        ar[j+1] = tmp;
    }
    System.out.println(Arrays.toString(ar));
}
  • 优化 - 折半插入排序
public static void main(String[]args){
    int[] arr = {5,3,4,6,2,1};
    int n = arr.length;
    int j,low,mid,high,temp;
    for(int i = 1;i < n;i ++){
        // 循环的起始数据
        low = 0;
        // 循环的循环数值
        high = i - 1;
        // 取第 i 个元素赋值到 tmp 中进行比较
        temp = arr[i];
        /* 找到合适的插入位置 high+1,如果中间位置元素
         * 比要插入元素大,则查找区域向低半区移动,否
         * 则向高半区移动
         */
        // 从前面的元素开始判断到 temp 的元素如果不满足则直接进行下一次循环
        while(low <= high){
        /* 每次执行将 low 与长度相加的值除 2 得出 mid 指针位置 */
            mid = (low+high)/2;
            // 判断 mid 的索引位置的值 是否 大于 temp 的值
            if(arr[mid] > temp){
                // 满足交换条件,指针位置 - 1, 缩短判断范围
                high = mid - 1;
            }else{
                // 不满足交换条件 指针位置 + 1, 缩短判断范围
                low = mid + 1;
            }
        }
        /*high+1 后的元素后移 */
        // 经过上面判断 i-1 为 temp 后一位的位置,从 high 循环到 i
        for(j = i - 1;j >= high + 1;j --) {
            arr[j + 1] = arr[j];
        }
        /* 将元素插入到指定位置 */
        arr[j + 1] = temp;
    }
    System.out.println(Arrays.toString(arr));
}
  • 优化 - 路插入排序
int j, first, last, mid;
int len = arr.length;
/* 临时数组 */
int[] tempArr =new int[len];
tempArr[0] = arr[0];
/*first 和 last 分别指临时数组 tempArr 中排好序的元素的第一个和最后一个位置 */
first = last = 0;
for(int i = 1; i<len; i++){
   /*j 是调整系数 */
   if(first > last){
      j = len;        
   }else{
      j = 0;
   }
   /*tempArr 中间元素的位置 */
   mid = ((first+last+j)/2)%len; 
   /*arr [i] 应该插入在 tempArr 的前半部分 */
   if(arr[i] < tempArr[mid]){      
      /*j 指向 tempArr 数组中的第一个元素 */
      j = first;
      /*first 前移,取余是为了实现循环数组效果 */
      first = (first-1+len)%len;  
      /* 待插元素大于 j 所指元素 */
      while(arr[i] > tempArr[j]){    
         /*j 所指元素前移,取余是为了实现循环数组效果 */
         tempArr[(j-1+len)%len] = tempArr[j];  
         /*j 指向下一个元素 */
         j = j+1;
      }
      /* 移动结束,待插元素插在 tempArr [j] 前 */
      tempArr[(j-1+len)%len] = arr[i];    
      /*arr [i] 应该插入在 tempArr 的后半部分 */
   }else{
      /*j 指向 tempArr 数组中的最后一个元素 */
      j = last;
      /*last 后移, 指向插入后的最后一个元素 */
      last++;
      /* 待插元素小于 j 所指元素 */
      while(arr[i] < tempArr[j]){  
         /*j 所指元素后移 */
         tempArr[(j+1)%len] = tempArr[j]; 
         /*j 指向上一个元素 */
         j = (j-1+len)%len;         
      }
      /* 移动结束,待插元素插在 tempArr [j] 后 */
      tempArr[(j+1)%len] = arr[i]; 
   }
}
/* 把在 tempArr 中排好序的元素依次赋给 arr*/
for(int i = 0; i < len; i++){                    
   arr[i] = tempArr[(first+i)%len];
}

# 希尔排序

简单插入排序存在的问题

我们看简单的插入排序可能存在的问题.

数组 arr = {2,3,4,5,6,1} 这时需要插入的数 1 (最小), 这样的过程是:

结论:当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.

希尔排序法介绍

希尔排序是希尔(Donald Shell)于 1959 年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。

希尔排序法基本思想

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止

希尔排序法 的示意图

代码实现:

交换法(效率较低)

public static void main(String[]args){
    int[] ar = {8,9,1,7,2,3,5,4,6,0};
    art(ar);
}
public static void art(int[] arr){
    int len = arr.length;
    // 每次执行外循环将数组的 长度 / 2 分开
    for (int gap = len/2;gap > 0;gap = gap/2) {
        // 从 / 2 的长度开始循环数组全部从 / 2 后的长度开始循环到数组长度
        for (int i = gap;i < len;i++) {
            // 将从 / 2 的长度开始进行遍历
            int j = i;
            // 判断循环 判断 j 是否大于等于 0 和 数组前后元素的大小
            while (j - gap >= 0 && arr[j] < arr[j - gap]) {
                // 调用排序方法
                sort(arr,j,j - gap);
                // 当 j-gap 还是满足 if 判断条件那么就看第二个条件是否满足如果满足则再次进行交换位置
                j = j - gap;
            }
        }
    }
}
public static void sort(int[] ar,int a,int b){
    int tmp = ar[b];
    ar[b] = ar[a];
    ar[a] = tmp;
    System.out.println(Arrays.toString(ar));
}

移动法(效率比交换高很多很多)

// 希尔排序
public static void shellSort(int[] arr){
   int len = arr.length;
   for(int gap = len / 2;gap > 0;gap = gap / 2){
      for(int  i = gap;i < len;i ++){
         // 使用插入排序的思路
         int j = i;
         int temp = arr[j];
         while(j - gap >= 0 && temp < arr[j - gap]){
            arr[j] = arr[j - gap];
            j -= gap;
         }
         arr[j] = temp;
      }
   }
}

# 快速排序

快速排序法介绍:

快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

快速排序法示意图:

以每个递归分支的最后一个元素为基准进行排序

以每个递归分支的最中间的元素为基准进行排序

代码实现:

@SuppressWarnings("all")
public class QuickSort {
    public static void main(String[] args) {
        int[] arr = {-1,-2,3,1,20,10,9,5};
        quickSort(arr,0,arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }
    public static void quickSort(int[] arr,int left,int right){
        int low = left;// 左下标
        int max = right;// 右下标
        // 中间下标
        int pivot = arr[(left + right) / 2];
        // 临时交换值变量
        int temp = 0;
        //while 循环的目的是让比 pivot 值小放到左边比 pivot 值大的放到右边
        while(low < max){
            // 在 pivot 的左边一直找,找到大于 pivot 值,才退出
            while(arr[low] < pivot){
                low += 1;
            }
            // 在 pivot 的右边一直找,找到小于 pivot 值,才退出
            while(arr[max] > pivot){
                max -= 1;
            }
            // 如果 low >= max 说明 pivot 的左右两边的值,已经按照左边全部是
            // 小于等于 pivot 的值,右边全部是大于等于 pivot 的值
            if(low >= max){
                break;
            }
            // 交换
            temp = arr[low];
            arr[low] = arr[max];
            arr[max] = temp;
            // 如果交换完后,发现这个 arr [low] == pivot 值 相等 则 max -- ,前移
            if(arr[low] == pivot){
                max -= 1;
            }
            // 如果交换完后,发现这个 arr [max] == pivot 值 相等 则 low ++ ,后移
            if(arr[max] == pivot){
                low += 1;
            }
            // 如果 low == max, 必须 low ++ , max -- , 否则会出现堆栈溢出
            if(low == max){
                low += 1;
                max -= 1;
            }
            // 向左递归
            if(left < max){
                quickSort(arr,left,max);
            }
            // 向右递归
            if(right > low){
                quickSort(arr,low,right);
            }
        }
    }
}

# 归并排序

归并排序介绍:

归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分 (divide) 成一些小的问题然后递归求解,而治 (conquer) 的阶段则将分的阶段得到的各答案 "修补" 在一起,即分而治之)。

归并排序思想示意图 1 - 基本思想:

说明:

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程。

归并排序思想示意图 2 - 合并相邻有序子序列:

再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将 [4,5,7,8] 和 [1,2,3,6] 两个已经有序的子序列,合并为最终序列 [1,2,3,4,5,6,7,8],来看下实现步骤

代码实现:

@SuppressWarnings("all")
public class GuibingSort {
    public static void main(String[] args) {
        int[] arr= {8,4,5,7,1,3,6,2,0};
        // 归并排序需要一个额外空间
        int[] temp = new int[arr.length];
        mergeSort(arr,0,arr.length - 1,temp);
        System.out.println(Arrays.toString(arr));
    }
    /**
     * 分 + 合方法
     * @param arr
     * @param left
     * @param right
     * @param temp
     */
    public static void mergeSort(int[] arr,int left,int right,int[] temp){
        if(left < right){
            // 中间索引
            int mid = (left + right) / 2;
            // 向左递归进行分解
            mergeSort(arr,left,mid,temp);
            // 向右递归进行分解
            mergeSort(arr,mid + 1,right,temp);
            // 合并
            merge(arr,left,mid,right,temp);
        }
    }
    /**
     * 合并方法
     * @param arr 排序的原始数据
     * @param left 左边有序序列的初始索引
     * @param mid 中间索引
     * @param right 右边索引
     * @param temp 做中转的数组
     */
    public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left;// 初始化 i,左边有序序列的初始索引
        int j = mid + 1;// 初始化 j,右边有序序列的初始索引
        int t = 0;// 指向 temp 数组的当前索引
        /**
         * (一)
         * 先把左右两边 (有序) 的数据按照规则填充到 temp 数组
         * 直到左右两边的有序序列,有一边处理完毕为止
         */
        while (i <= mid && j <= right) {
            /**
             * 如果左边有序序列的当前元素,小于等于右边有序的当前元素
             * 即将左边的当前元素,拷贝到 temp 数组
             * 然后 t ++ i ++
             */
            if (arr[i] <= arr[j]) {
                temp[t] = arr[i];
                t += 1;
                i += 1;
            }else{
                temp[t] = arr[j];
                j += 1;
                t += 1;
            }
        }
        /**
         * (二)
         * 把有剩余数据的一边的数据依次全部填充到 temp
         */
        // 左边的有序序列还有剩余的元素,就全部填充到 temp
        while(i <= mid){
            temp[t] = arr[i];
            t += 1;
            i += 1;
        }
        // 右边的有序序列还有剩余的元素,就全部填充到 temp
        while(j <= right){
            temp[t] = arr[j];
            t += 1;
            j += 1;
        }
        /**
         * (三)
         * 将 temp 数组的元素拷贝到 arr
         * 注意:并不是每次都拷贝所有的数据
         */
        t = 0;
        int tempLeft = left;
        /**
         * 第一次合并 tempLeft = 0, right = 1
         * 第二次合并 tempLeft = 2 right = 3
         * 第三次合并 tempLeft = 0 right = 3
         * 最后一次 tempLeft = 0 right = 7
         */
        while(tempLeft <= right){
            arr[tempLeft] = temp[t];
            t += 1;
            tempLeft += 1;
        }
    }
}

# 基数排序

基数排序 (桶排序) 介绍:

  1. 数排序(radix sort)属于 “分配式排序”(distribution sort),又称 “桶子法”(bucket sort)或 bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些 “桶” 中,达到排序的作用

  2. 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法

  3. 基数排序 (Radix Sort) 是桶排序的扩展

  4. 基数排序是 1887 年赫尔曼・何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。

基数排序基本思想

  1. 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

  2. 这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤

基数排序图文说明

将数组 {53, 3, 542, 748, 14, 214} 使用基数排序,进行升序排序。

第 1 轮排序 [按照个位排序]:

说明: 事先准备 10 个数组 (10 个桶), 0-9 分别对应 位数的 0-9

  1. 将 各个数,按照个位大小 放入到 对应的 各个数组中
  2. 然后从 0-9 个数组 / 桶,依次,按照加入元素的先后顺序取出

示意图:

分解代码:

@SuppressWarnings("all")
public class JichuSort {
    public static void main(String[] args) {
        int[] arr = {53,3,542,748,14,214};
        radixSort(arr);
    }
    // 基数排序方法
    public static void radixSort(int[] arr){
        /**
         * 第一轮(针对每个元素的个位进行排序处理)
         * 定义一个二维数组,表示 10 个桶,每个桶就是一个一维数组
         * 说明:
         * 1. 二维数组包含 10 个一维数组
         * 2. 为了防止在放入数值的时候,数据溢出,则每个一维数组(桶),大小定义为 arr.length
         * 3. 名明确,基础排序是使用空间换时间的经典算法
         */
        int[][] bucket = new int[10][arr.length];
        /**
         * 为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
         * 可以这么理解:
         * 比如:bucketElementCounts [0], 记录的就是 bucket [0] 桶的放入数据个数
         */
        int[] bucketElementCounts = new int[10];
        // 第一轮(针对每个元素的个位进行排序处理)
        for(int j = 0;j < arr.length;j ++){
            // 取出每个元素的个位的值
            int digitOfElement = arr[j] % 10;
            // 放入到对应的桶中
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
            bucketElementCounts[digitOfElement] ++;
        }
        // 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
        int index = 0;
        // 遍历每一桶,并将桶中的数据,放入到原数组
        for(int k = 0;k < bucketElementCounts.length;k ++){
            // 如果桶中,有数据,我们才放入原数组
            if(bucketElementCounts[k] != 0){
                // 循环该桶即第 k 个桶(即第 k 个一维数组),放入
                for(int l = 0;l < bucketElementCounts[k];l ++){
                    // 取出元素放入到 arr
                    arr[index ++] = bucket[k][l];
                }
            }
            bucketElementCounts[k] = 0;
        }
        System.out.println("第一轮排序结果:"+Arrays.toString(arr));
        //======================================
        // 第二轮 (针对十位进行排序)
        for(int j = 0;j < arr.length;j ++){
            // 取出每个元素的十位的值
            int digitOfElement = arr[j] / 10 % 10;
            // 放入到对应的桶中
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
            bucketElementCounts[digitOfElement] ++;
        }
        // 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
        index = 0;
        // 遍历每一桶,并将桶中的数据,放入到原数组
        for(int k = 0;k < bucketElementCounts.length;k ++){
            // 如果桶中,有数据,我们才放入原数组
            if(bucketElementCounts[k] != 0){
                // 循环该桶即第 k 个桶(即第 k 个一维数组),放入
                for(int l = 0;l < bucketElementCounts[k];l ++){
                    // 取出元素放入到 arr
                    arr[index ++] = bucket[k][l];
                }
            }
            bucketElementCounts[k] = 0;
        }
        System.out.println("第二轮排序结果:"+Arrays.toString(arr));
        //==========================================
        // 第三轮 (针对百位进行排序)
        for(int j = 0;j < arr.length;j ++){
            // 取出每个元素的百位的值
            int digitOfElement = arr[j] / 100 % 10;
            // 放入到对应的桶中
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
            bucketElementCounts[digitOfElement] ++;
        }
        // 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
        index = 0;
        // 遍历每一桶,并将桶中的数据,放入到原数组
        for(int k = 0;k < bucketElementCounts.length;k ++){
            // 如果桶中,有数据,我们才放入原数组
            if(bucketElementCounts[k] != 0){
                // 循环该桶即第 k 个桶(即第 k 个一维数组),放入
                for(int l = 0;l < bucketElementCounts[k];l ++){
                    // 取出元素放入到 arr
                    arr[index ++] = bucket[k][l];
                }
            }
        }
        System.out.println("第三轮排序结果:"+Arrays.toString(arr));
    }
}

规律:每轮求数值的位数值一直在改变其它不变

基数排序实现:

@SuppressWarnings("all")
public class JichuSort {
    public static void main(String[] args) {
        int[] arr = {53, 3, 542, 748, 14, 214};
        radixSort(arr);
    }
    // 基数排序方法
    public static void radixSort(int[] arr) {
        // 根据前面的推到过程,我们可以得到最终的基数排序代码
        //1. 得到数组中最大的数的位数
        int max = arr[0];// 假设第一个元素就是最大为数
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        // 得到最大数是几位数
        int maxLength = (max + "").length();
        /**
         * 第一轮(针对每个元素的个位进行排序处理)
         * 定义一个二维数组,表示 10 个桶,每个桶就是一个一维数组
         * 说明:
         * 1. 二维数组包含 10 个一维数组
         * 2. 为了防止在放入数值的时候,数据溢出,则每个一维数组(桶),大小定义为 arr.length
         * 3. 名明确,基础排序是使用空间换时间的经典算法
         */
        int[][] bucket = new int[10][arr.length];
        /**
         * 为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
         * 可以这么理解:
         * 比如:bucketElementCounts [0], 记录的就是 bucket [0] 桶的放入数据个数
         */
        int[] bucketElementCounts = new int[10];
        for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {
            // 第一轮(针对每个元素的个位进行排序处理)
            for (int j = 0; j < arr.length; j++) {
                // 取出每个元素的个位的值
                int digitOfElement = arr[j] / n % 10;
                // 放入到对应的桶中
                bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
                bucketElementCounts[digitOfElement]++;
            }
            // 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
            int index = 0;
            // 遍历每一桶,并将桶中的数据,放入到原数组
            for (int k = 0; k < bucketElementCounts.length; k++) {
                // 如果桶中,有数据,我们才放入原数组
                if (bucketElementCounts[k] != 0) {
                    // 循环该桶即第 k 个桶(即第 k 个一维数组),放入
                    for (int l = 0; l < bucketElementCounts[k]; l++) {
                        // 取出元素放入到 arr
                        arr[index++] = bucket[k][l];
                    }
                }
                bucketElementCounts[k] = 0;
            }
            System.out.println("第"+i+"轮排序结果:" + Arrays.toString(arr));
        }
    }
}

# 常用排序算法总结和对比

常用排序算法对比

相关术语解释

  1. 稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面;
  2. 不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面;
  3. 内排序:所有排序操作都在内存中完成;
  4. 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
  5. 时间复杂度: 一个算法执行所耗费的时间。
  6. 空间复杂度:运行完一个程序所需内存的大小。
  7. n: 数据规模
  8. k: “桶” 的个数
  9. In-place: 不占用额外内存
  10. Out-place: 占用额外内存

# 查找算法

查找算法介绍

在 java 中,我们常用的查找有四种:

  1. 顺序 (线性) 查找
  2. 二分查找 / 折半查找
  3. 插值查找
  4. 斐波那契查找

# 线性查找算法

<font style="color:red"> 要求数组是一个有序的数组 </font>.

有一个数列: {1,8, 10, 89, 1000, 1234} ,判断数列中是否包含此名称【顺序查找】 要求:如果找到了,就提示找到,并给出下标值。

思路:如果查找到全部符合条件的值。[思路分析.]

public static void main(String[] args)
    {
        int arr[] = {1, 2, 3, 4, 5, 6};
        int i = lineSearch(arr, 5);
        System.out.println(i);
    }
    private static int lineSearch(int arr[], int value)
    {
        for(int i = 0; i < arr.length; i++)
        {
            if(arr[i] == value)
            {
                return i;
            }
        }
        return -1;
    }

打印结果:

4

# 二分查找算法

二分查找

<font style="color:red"> 要求数组是一个有序的数组 </font>.

请对一个有序数组进行二分查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示 "没有这个数"。

课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中,有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000.

代码实现:

public class Binary_search {
    // 二分查找
    public static void main(String[]args){
        int[] ar = {1,3,5,7,8,9};
        int x = run(ar,8);
        System.out.println(x);
    }
    public static int run(int[] nums,int trans){
        int mid = 0;
        int first = 0;
        int last = nums.length - 1;
        // 判断 目标值是否小于索引 0 的值,目标值是否大于索引长度的值,长度
        if(trans < nums[first] || trans > nums[last] || first > last)
            return -1;
        // 判断循环
        while(first <= last){
            // 将长度除 2 的值赋值给 mid, 起始位置,访问数组中间的数据
            mid = (first + last) / 2;
            // 判断数字是否小了
            if(nums[mid] > trans)
                // 如果小了则让 last - 1 然后再次除 2 判断数字的大小
                last = mid - 1;
            //else 只执行一个结果
            else
                // 判断数字是否大了
            if(nums[mid] < trans)
                // 如果数字大了则 first + 1 然后再次除 2 判断数字的大了,每次都是一半一半的进行试探的判断
                first = mid + 1;
            //else 只执行一个结果
            else
                // 如果两个判断不满足则执行返回值当前的索引位置就是目标值
            return mid;
        }
        // 如果全程没有找到则返回 -1
        return -1;
    }
}

递归实现:

@SuppressWarnings("all")
public class Chazhao01 {
    public static void main(String[] args) {
        int[] arr = {1,8, 10, 89, 1000, 1000,1234};
        int i = binarySearch(arr, 0, arr.length - 1, 89);
        System.out.println(i);
        List<Integer> i1 = binarySearch1(arr, 0, arr.length - 1, 1000);
        System.out.println("多个值的下标:"+i1);
    }
    /**
     * @param arr 数组
     * @param left 左边的索引
     * @param right 右边的索引
     * @param value 要查找的值
     * @return 如果找到就返回下标,否则返回 - 1
     */
    public static int binarySearch(int[] arr,int left,int right,int value){
        if(left > right){
            return - 1;
        }
        int mid = (left + right) / 2;
        int midValue = arr[mid];
        if(value > midValue){// 向右递归
            return binarySearch(arr,mid + 1,right,value);
        }else if(value < midValue){// 向左递归
            return binarySearch(arr,left,mid - 1,value);
        }else {
            return mid;
        }
    }
    public static List<Integer> binarySearch1(int[] arr,int left,int right,int value){
        if(left > right){
            return new ArrayList<>();
        }
        int mid = (left + right) / 2;
        int midValue = arr[mid];
        if(value > midValue){// 向右递归
            return binarySearch1(arr,mid + 1,right,value);
        }else if(value < midValue){// 向左递归
            return binarySearch1(arr,left,mid - 1,value);
        }else{
            /**
             * 思路分析
             * 1. 在找到 mid 索引值,不要马上返回
             * 2. 向 mid 索引值的左边扫描,将所有满足 value 的元素下标加入集合中
             * 3. 向 mid 索引值的右边扫描,将所有满足 value 的元素下标加入集合中
             * 4. 将集合返回
             */
            List<Integer> list = new ArrayList<>();
            // 向 mid 索引值的左边扫描,将所有满足 value 的元素的下标加入集合中
            int temp = mid - 1;
            while(true){
                if(temp < 0 || arr[temp] != value){// 结束
                    break;
                }
                // 否则,将 temp 放入到集合中
                list.add(temp);
                //temp 左移
                temp -= 1;
            }
            list.add(mid);
            temp = mid + 1;
            while(true){
                if(temp > arr.length - 1 || arr[temp] != value){
                    break;
                }
                list.add(temp);
                temp += 1;
            }
            return list;
        }
    }
}

# 插值查找算法

插值查找原理介绍:

  1. 插值查找算法类似于二分查找,不同的是插值查找每次从自适应 mid 处开始查找。

  2. 将折半查找中的求 mid 索引的公式,low 表示左边索引 left, high 表示右边索引 right. key 就是前面我们讲的 findVal

  3. int mid = low + (high - low) * (key - arr[low]) / (arr[high] -arr[low]) ;/ 插值索引 /
    对应前面的代码公式:
    int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left])

  4. 举例说明插值查找算法 1-100 的数组

代码实现:

@SuppressWarnings("all")
public class InsertValueSearch {
    public static void main(String[] args) {
        int[] arr = new int[100];
        for(int i = 1;i <= 100;i ++){
            arr[(i - 1)] += i;
        }
        System.out.println(insertValuesearch(arr,0,arr.length - 1,100));
    }
    /**
     * @param arr 数组
     * @param left 左边索引
     * @param right 右边索引
     * @param value 查找值
     * @return 如果找到,返回对应下标,没有返回 -1
     */
    public static int insertValuesearch(int[] arr,int left,int right,int value){
        /**
         * 注意:value < arr [0] 和 value > arr [arr.length - 1] 必须需要
         * 否则我们得到的 mid 可能越界
         * arr.length - 1 < right 判断传入的 right 值是否大于了传入数组的长度
         */
        if(left > right || value < arr[0] || value > arr[arr.length - 1] || arr.length - 1 < right){
            return -1;
        }
        // 求 mid 的值 称为自适应写法
        int mid = left + (right - left) * (value - arr[left]) / (arr[right] - arr[left]);
        int midVal = arr[mid];
        if (value > midVal) {
            // 向右边递归
            return insertValuesearch(arr,mid + 1,right,value);
        }else if(value < midVal) {
            // 向左边递归
            return insertValuesearch(arr,left,mid - 1,value);
        }else{
            return mid;
        }
    }
}

<font style="color:red"> 插入查找注意事项 </font>:

  1. 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找 速度较快.
  2. 关键字分布不均匀的情况下,该方法不一定比折半查找要好

# 斐波那契 (黄金分割法) 查找算法

斐波那契 (黄金分割法) 查找基本介绍:

  1. 黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是 0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意向不大的效果。

  2. 斐波那契数列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55} 发现斐波那契数列的两个相邻数 的比例,无限接近 黄金分割值 0.618

斐波那契 (黄金分割法) 查找算法

斐波那契 (黄金分割法) 原理:

斐波那契查找原理与前两种相似,仅仅 改变了中间结点(mid)的位置,mid 不 再是中间或插值得到,而是位于黄金分 割点附近,即 mid=low+F (k-1)-1 (F 代表斐波那契数列),如下图所示
对 F (k-1)-1 的理解:

  1. 由斐波那契数列 F [k]=F [k-1]+F [k-2] 的性质,可以得到 (F [k]-1)=(F [k-1]-1)+(F [k-2]-1)+1 。该式说明:只要顺序表的长度为 F [k]-1,则可以将该表分成长度为 F [k-1]-1 和 F [k-2]-1 的两段,即如上图所示。从而中间位置为 mid=low+F (k-1)-1
  2. 类似的,每一子段也可以用相同的方式分割
  3. 但顺序表长度 n 不一定刚好等于 F [k]-1,所以需要将原来的顺序表长度 n 增加至 F [k]-1。这里的 k 值只要能使得 F [k]-1 恰好大于或等于 n 即可,由以下代码得到,顺序表长度增加后,新增的位置(从 n+1 到 F [k]-1 位置),都赋为 n 位置的值即可。

斐波那契查找应用案例

请对一个有序数组进行斐波那契查找 {1,8, 10, 89, 1000, 1234} ,输入一个数看看该数组是否存在此数,并且求出下标,如果没有就提示 "没有这个数"。

代码实现

@SuppressWarnings("all")
public class FibonnacciSearch {
    public static int maxSize = 20;
    public static void main(String[] args) {
        int[] arr = {1,8, 10, 89, 1000, 1234};
        int i = fibSearch(arr, 89);
        System.out.println("index : "+i);
        System.out.println(Arrays.toString(fib()));
    }
    /**
     * 因为后面我们 mid = low + f [k - 1] - 1; 需要使用到斐波那契数列,因此我们需要先获取到一个斐波那契数列
     * 非递归方法得到一个斐波那契数列
     * @return
     */
    public static int[] fib(){
        int[] f = new int[maxSize];
        f[0] = 1;
        f[1] = 1;
        for(int i = 2;i < maxSize;i ++){
            f[i] = f[i - 1] + f[i - 2];
        }
        return f;
    }
    /**
     * 编写斐波那契数列查找算法
     * 使用递归方式编写算法
     * @param a 数组
     * @param key 我们需要查找的关键值
     * @return 返回对应的下标,如果没有返回 - 1
     */
    public static int fibSearch(int[] a,int key){
        int low = 0;
        int high = a.length - 1;
        int k = 0;// 表示斐波那契数列分割数值的下标
        int mid = 0;// 存放 mid 的值
        int[] f = fib();
        //f[k] = 1,1,2,3,5,8 (8 - 1 = 7) ==> false k == 5
        while(high > f[k] - 1){
            k ++;
        }
        /**
         * 因为 f [k] 值可能大于 a 的长度,因此我们需要使用 Arrays 类,构造一个新数组,并指向 a []
         * 不足的部分会使用 0 填充
         * [1, 8, 10, 89, 1000, 1234, 0, 0]
         */
        int[] temp = Arrays.copyOf(a,f[k]);
        /**
         * 实际上需求使用 a 数组最后的数填充 temp
         * 举例:
         * temp = {1,8, 10, 89, 1000, 1234} => {1,8, 10, 89, 1000, 1234,1234,1234}
         */
        for(int i = high + 1;i < temp.length;i ++){
            temp[i] = a[high];
        }
        // 使用 while 来循环处理,找到我们的数 key
        while(low <= high){
            mid = low + f[k - 1] - 1;
            if(key < temp	[mid]){
                high = mid - 1;
                /**
                 * 为什么是 k--
                 * 说明:
                 * 1. 全部元素 = 前面的元素 + 后面元素
                 * 2.f [k] = f [k - 1] + f [k - 2];
                 * 因为前面有 f [k - 1] 个元素,所以可以继续拆分 f [k - 1] = f [k - 2] + f [k - 3]
                 * 即在 f [k - 1] 的前面继续查找 k --
                 * 即下次循环 mid = f [k - 1 - 1] - 1
                 */
                k--;
            }else if(key > temp[mid]){// 我们应该继续向数组的后面查找 (右边)
                low = mid + 1;
                /**
                 * 为什么是 k -= 2;
                 * 说明:
                 * 1. 全部元素 = 前面元素 + 后面元素
                 * 2.f [k] = f [k - 1]  + f [k - 2]
                 * 3. 因为后面我们有 f [k - 2] 所以可以继续拆分 f [k - 1] = f [k - 3] + f [k - 4]
                 * 4. 即在 f [k - 2] 的前面进行查找 k -= 2
                 * 5. 即下次循环 mid = f [k - 1 - 2] - 1
                 */
                k -= 2;
            }else{
                // 需要确定,返回的是哪个下标
                if(mid <= high){
                    return mid;
                }else{
                    return high;
                }
            }
        }
        return -1;
    }
}

# 哈希表

看一个实际需求,google 公司的一个上机题:

有一个公司,当有新的员工来报道时,要求将该员工的信息加入 (id, 性别,年龄,住址..), 当输入该员工的 id 时,要求查找到该员工的 所有信息.

要求:不使用数据库,尽量节省内存,速度越快越好 => 哈希表 (散列)

哈希表的基本介绍

散列表(Hash table,也叫哈希表),是根据关键码值 (Key value) 而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列

15 111 % 15

哈希表 (散列)- 应用实例

google 公司的一个上机题:

有一个公司,当有新的员工来报道时,要求将该员工的信息加入 (id, 性别,年龄,名字,住址..), 当输入该员工的 id 时,要求查找到该员工的 所有信息.

要求:

不使用数据库,速度越快越好 => 哈希表 (散列)

添加时,保证按照 id 从低到高插入 [课后思考:如果 id 不是从 低到高插入,但要求各条链表仍是从低到高,怎么解决?]

  1. 使用链表来实现哈希表,该链表不带表头 [即:链表的第一个结点就存放雇员信息]
  2. 思路分析并画出示意图
  3. 代码实现 [增删改查 (显示所有员工,按 id 查询)]

示意图:

代码实现:

@SuppressWarnings("all")
public class HashTableDemo {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        HashTable hashTable = new HashTable(7);
        char ch = ' ';
        boolean flag = true;
        while(flag){
            System.out.println("l(list) 查看所有雇员信息");
            System.out.println("a(add) 添加雇员信息到链表");
            System.out.println("s(select) 根据id查找指定雇员");
            System.out.println("d(delet) 根据id删除指定雇员信息,并返回信息");
            System.out.println("D(delet) 根据id删除指定雇员信息");
            System.out.println("e(exit) 退出");
            System.out.println("=======================");
            ch = sc.next().charAt(0);
            switch(ch){
                case 's':
                    System.out.println("请输入需要查找的雇员id");
                    int id1 = sc.nextInt();
                    hashTable.findEmpById(id1);
                    break;
                case 'l':
                    hashTable.list();
                    break;
                case 'a':
                    System.out.println("请输入id");
                    int id = sc.nextInt();
                    System.out.println("请输入名称");
                    String name = sc.next();
                    Emp emp = new Emp(id,name);
                    hashTable.add(emp);
                    break;
                case 'd':
                    try{
                        System.out.println("请输入要删除的雇员id");
                        int id2 = sc.nextInt();
                        Emp emp1 = hashTable.delEmp(id2);
                        System.out.printf(">>>\tData:\t%s\n",emp1);
                    }catch(Exception e){
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'D':
                    try {
                        System.out.println("请输入要删除的雇员id");
                        int vlaue = sc.nextInt();
                        hashTable.delEmp01(vlaue);
                    } catch (Exception e) {
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'e':
                    sc.close();
                    flag = false;
                    break;
                default:
                    System.out.println("选择存在项");
                    break;
            }
        }
    }
}
@SuppressWarnings("all")
// 创建 HashTable 管理多条链表
class HashTable{
    private EmpLinkedList[] empLinkedListArray;
    // 表示共有多少条链表 (链表的大小)
    private int size;
    // 构造器,初始化链表
    public HashTable(int size) {
        // 初始化 size 值
        this.size = size;
        // 初始化数组大小
        empLinkedListArray = new EmpLinkedList[size];
        //TODO 注意:每一个链表都需要初始化否则报错空指针异常
        for(int i = 0;i < size;i ++){
            empLinkedListArray[i] = new EmpLinkedList();
        }
    }
    // 添加雇员
    public void add(Emp emp){
        // 根据员工的 id,得到该员工应当添加到哪条链表
        int num = hashFun(emp.id);
        // 将 emp 添加到对应的链表中
        empLinkedListArray[num - 1].add(emp);
    }
    // 遍历所有的链表 (遍历 HashTable)
    public void list(){
        for(int i = 0;i < size;i ++){
            empLinkedListArray[i].list(i);
        }
    }
    // 编写散列函数,使用一个简单取模法
    public int hashFun(int id){
        return id % size;
    }
    // 根据 id 查找雇员
    public void findEmpById(int id){
        int num = hashFun(id);
        Emp emp = empLinkedListArray[num - 1].findById(id);
        if(emp != null){
            System.out.printf("在第%d个链表中查找到 的雇员信息如下:\n>>> id: %d\n>>> name: %s",num,emp.id,emp.name+"\n");
        }else{
            System.out.println("抱歉>"+num+"<链表中没有找到任何信息");
        }
    }
    public Emp delEmp(int id){
        int num = hashFun(id);
        Emp del = empLinkedListArray[num - 1].del(id);
        return del;
    }
    public void delEmp01(int id){
        int num = hashFun(id);
        empLinkedListArray[num - 1].del01(id);
    }
}
@SuppressWarnings("all")
// 表示一个雇员
class Emp{
    public int id;
    public String name;
    public Emp next;
    public Emp(int id, String name) {
        this.id = id;
        this.name = name;
    }
    @Override
    public String toString() {
        return "Emp{" +
                "id=" + id +
                ", name='" + name + '\'';
    }
}
@SuppressWarnings("all")
// 表示链表
class EmpLinkedList{
    // 头指针,执行第一个 Emp,因此我们这个链表的 head 是直接指向第一个 Emp
    private Emp head;
    /**
     * 添加雇员到链表
     * 说明:
     * 1. 假设,当添加雇员时,id 是自增长,即 id 的分配总是从小到大
     * 因此我们将该雇员直接加入到本链表的最后即可
     * @param emp
     */
    public void add(Emp emp){
        // 如果是添加第一个雇员
        if(head == null){
            head = emp;
            return;
        }
        // 如果不是第一个雇员,则使用一个辅助指针,帮助定位到最后
        Emp curEmp = head;
        while(true){
            // 说明到链表最后
            if(curEmp.next == null)
                break;
            // 指针后移
            curEmp = curEmp.next;
        }
        // 退出时直接将 tmp 加入到链表即可
        curEmp.next = emp;
    }
    /**
     * 遍历链表的雇员信 a
     */
    public void list(int no){
        if(head == null){
            System.out.println("第>"+(no + 1)+"<个链表为空");
            return;
        }
        System.out.println("*-==第>"+(no + 1)+"<个链表中的雇员的信息为==-*");
        Emp curEmp = head;
        while(true){
            System.out.printf(">>> id: %d \n>>> name: %s",curEmp.id,curEmp.name+"\n");
            // 判断是否遍历到链表的最后
            if(curEmp.next == null)
                break;
            // 指针后移
            curEmp = curEmp.next;
        }
    }
    // 根据 id 查找雇员信息
    public Emp findById(int id){
        if(head == null){
            return null;
        }
        Emp curEmp = head;
        boolean flag = false;
        while(true){
            if(curEmp.id == id){
                flag = true;
                break;
            }
            if(curEmp.next == null){
                break;
            }
            curEmp = curEmp.next;
        }
        if(flag){
            return curEmp;
        }else{
            return null;
        }
    }
    // 根据 id 删除雇员,并返回删除的雇员信息
    public Emp del(int id){
        if(head == null){
            throw new RuntimeException(">>>"+id+"<链表为空删除无效");
        }
        // 赋值指针
        Emp curEmp = head;
        // 记录上一个位置指针
        Emp pre = head;
        while(true){
            if(curEmp.id == id){
                break;
            }
            if(curEmp == null){
                curEmp = null;
                break;
            }
            // 将当前节点赋值给 pre
            pre = curEmp;
            //curEmp 节点向后移动
            curEmp = curEmp.next;
        }
        // 判断当前节点是否为 null
        if(curEmp == null){
            return null;
        }
        // 判断头节点是否和当前节点相同
        if(head == curEmp){
            // 将 curEmp.next 元素赋值给头节点
            head = curEmp.next;
            return curEmp;
        }
        // 将赋值的元素再赋值给 pre 的 next
        pre.next = curEmp.next;
        return curEmp;
    }
    // 根据 id 删除雇员
    public void del01(int id){
        if(head == null){
            throw new RuntimeException(">>>"+id+"<链表为空删除无效");
        }
        Emp curEmp = head;
        boolean flag = false;
        while(true){
            if(curEmp.id == id){
                flag = true;
                break;
            }
            if(curEmp == null){
                break;
            }
            curEmp = curEmp.next;
        }
        if(flag){
            System.out.println("该"+id+"雇员删除成功");
            // 判断下一个 next 节点是否为 null
            if(curEmp.next == null){
                // 如果下一个 next 就是 null 那么就说明了这是第一个元素直接将头元素赋值为 null 即可
                head = null;
            }else{
                // 否则将下一个元素赋值为 null
                curEmp.next = null;
            }
        }else{
            System.out.println("您输入的雇员可能本来就不存在");
        }
    }
}