0%

【数据结构和算法】排序算法

排序算法,是算法基础入门,下面介绍八种的排序算法的实现


1 排序算法

1.1 介绍

  • 排序,也称为排序算法(Sort Algorithm),排序一组数据,依指定的顺序进行排序的过程
  • 排序分类:
  • (1)内部排序:将需要处理的所有数据都加载到内部存储器中进行排序
  • (2)外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序

2 冒泡排序

2.1 介绍

  • 冒泡排序(Bubble Sorting) 的基本思想为:通过遍历比较序列,依次比较当前index,和index+1是否逆序,逆序则交换两者位置,最后变成最小 / 最大的放在末尾位置,后续遍历依次放倒数第二,倒数第三…等位置,以此类推,遍历结束则为有序序列。就仿佛水底下的气泡逐渐上升
  • 时间复杂度:O(n^2)
  • 算法优化:如果一趟遍历下没有进行过任何交互,证明序列为有序,就直接停止循环遍历。可以设置一个变量来记录一次遍历中是出现交换

2.2 图解算法

2.3 代码

  • (1)基础代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class BubbleSort {

    /**
    * 升序排序
    * @param arr 排序序列
    */
    public static void asc(int[] arr){
    int n = arr.length; //数组长度
    int temp; //中间变量

    //遍历次数为:长度-1 次
    for(int i=0; i<n-1; i++) {
    //比较次数为:n-当前遍历次数(因为i从0开始,要额外-1,即n-(i+1))
    for (int j=0; j<n-i-1; j++) {
    if (arr[j] > arr[j+1]) {
    //利用中间变量交换位置
    temp = arr[j];
    arr[j] = arr[j+1];
    arr[j+1] = temp;
    }
    }
    }
    }

    }
  • (2)测试
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Test
    public void bubbleSortTest() {
    //创建数组,并调用方法排序
    int[] arr = new int[]{2, 1, 5, -2};
    BubbleSort.asc(arr);

    for (int item : arr) {
    System.out.printf("%d ", item);
    }
    }

2.4 优化代码

  • 在上面基础代码上添加一个变量记录是否发生交换,未发生交换,序列为有序,方法退出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 升序排序
* @param arr 排序序列
*/
public static void asc(int[] arr){
int n = arr.length;
int temp;
boolean isSwitch; //记录是否交换

for(int i=0; i<n-1; i++) {
isSwitch = false; //默认设置为false

for (int j=0; j<n-i-1; j++) {
if (arr[j] > arr[j+1]) {
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
//记录已发生交换
isSwitch = true;
}
}

//未发生交换,序列为有序,直接结束方法
if (!isSwitch) {
return;
}
}
}

3 选择排序

3.1 介绍

  • 选择排序(select sorting):属于内部排序法,从欲排序的数据中,按照指定的规则选择出来摸一个元素,在依规定交换位置后大大排序的目的
  • 算法思路:遍历数据,选出最小 / 最大的数据,将其交换防止数组开启;后续的遍历类型,选出最小 / 最大 放在后一位
  • 与冒泡区别:判断的次数几乎一样,但数据之间的交换次数变少,速度比冒泡快
  • 算法优化:如果当前起始位置刚好就是最小值,则无需交换,添加一个if判断来处理

3.2 图解算法

3.3 代码

  • (1)代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class SelectSort {

    /**
    * 选择排序 - 升序
    * @param arr 排序序列
    */
    public static void asc(int[] arr) {
    int n = arr.length; //数组大小
    int temp; //中间变量:存储最小值
    int index; //记录最小值的下标

    //遍历次数:n-1次
    for(int i=0; i<n-1; i++) {

    //遍历数据,起始位置为当前遍历次数-1(即 i)
    //默认将起始位置(i)数据作为最小值
    temp = arr[i];
    index = i;

    //因为i默认为最小值,无需跟自己比,j从i+1开始比较
    for(int j=i+1; j<n; j++) {
    //发现更小的值,则记录其值和下标
    if (arr[j] < temp) {
    temp = arr[j];
    index = j;
    }
    }

    //找完最小值,利用中间变量交换数据
    arr[index] = arr[i];
    arr[i] = temp;
    }
    }
    }
  • (2)测试
    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    public void selectSortTest() {
    //创建数据并排序
    int[] arr = new int[]{2, 1, 5, -2};
    SelectSort.asc(arr);

    System.out.println(Arrays.toString(arr));
    }

3.4 代码优化

  • 添加一个判断,如果当前起始位置就是最小值,则无需进行交换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 选择排序 - 升序
* @param arr 排序序列
*/
public static void asc(int[] arr) {
int n = arr.length;
int temp;
int index;


for(int i=0; i<n-1; i++) {
temp = arr[i];
index = i;
for(int j=i+1; j<n; j++) {
if (arr[j] < temp) {
temp = arr[j];
index = j;
}
}

//如果起始位置 == 最小值,无需进行交换
if(i != index) {
arr[index] = arr[i];
arr[i] = temp;
}
}
}

4 插入排序

4.1 介绍

  • 插入排序(insert sorting):属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,已达到排序目的
  • 算法思路:把n个待排序的元素分为有序表和无序表,每次遍历从无序表中,取出一个元素,跟有序表中的数据进行比较,找到合适的位置,进行插入
  • 插入方法:待插入数据跟前一个数据比较大小,大小顺序不对,则前一个数据进行后移,再跟前前一个数据比较,直到大小顺序合适 或 已经没有前一个数据
  • 跟冒泡比较:看起来和冒泡相似,一个将数据往后移,一个将数据往前移;但是插入排序只是将数据后移,并没有交换两者之间的数据,操作步骤对比冒泡是少了一步,算法速度更快

4.2 算法图解

4.3 代码

  • (1)代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class InsertSort {
    /**
    * 插入排序 - 升序
    * @param arr 序列
    */
    public static void asc(int[] nums) {
    // i表示当前要插入的数据
    for (int i=1; i<nums.length; i++) {
    int idx = i;
    int temp = nums[i];
    // i开始,从后往前比较,插入数小于前数,则前数后移
    while (idx > 0 && temp < nums[idx-1]) {
    nums[idx] = nums[idx-1];
    idx --;
    }
    // idx发生变化,数据发生后移,则插入数据
    if (idx != i) {
    nums[idx] = temp;
    }
    }
    }
    }
  • (2)测试
    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    public void insertSortTest() {
    //创建数据并排序
    int[] arr = new int[]{2, 1, 5, -2};
    InsertSort.asc(arr);

    System.out.println(Arrays.toString(arr));
    }

5 希尔排序

5.1 介绍

  • 希尔排序(shell sorting):该算法由希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后一个更高效的版本,也称为缩小增量排序
  • 算法思想:对序列按指定步长进行分组,对每个组执行一次排序算法。然后不断缩小分组,最后合成一个组,执行排序算法,形成有序序列
  • 优点:将数据缩小进行排序,速度快之余,并将避免很小的数在最后,需要一步一步往前挪的情况,算法运行的时间比其他算法相对于稳定

5.2 算法图解

5.3 希尔排序-交换法(不推荐)

  • 代码实现:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /**
    * 希尔排序-交换法(冒泡排序)-升序
    * @param arr 待排序队列
    */
    public static void ascBySwitch(int[] arr) {
    int n = arr.length; //数组长度
    int temp; //中间变量(用于交换)
    int step = n/2; //步长

    //因为步长为1时,再计算步长1/2=0,会退出循环
    //前两个循环用于分组
    while(step > 0) {
    for (int i=step; i<n; i++) {
    //类似冒泡排序,只不过是从后往前按照指定步长比较(效率慢)
    for (int j=i-step; j>=0; j-=step) {
    if (arr[j] > arr[j+step]) {
    temp = arr[j];
    arr[j] = arr[j+step];
    arr[j+step] = temp;
    }
    }
    }
    //修改步长
    step = step / 2;
    }
    }

5.4 希尔排序-移位法(推荐)

  • 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    /**
    * 希尔排序-移位法(插入排序)-升序
    * @param arr 待排序队列
    */
    public static void ascByMove(int[] arr) {
    int n = arr.length; //队列长度
    int step = n/2; //步长
    int temp; //记录待插入数据
    int index; //记录待插入数据的下标

    //前两个循环用于分组
    while (step > 0) {
    for (int i=step; i<n; i++) {
    //记录待插入数据
    temp = arr[i];
    index = i;

    while(index-step>=0 && temp<arr[index-step]) {
    //数据后移
    arr[index] = arr[index - step];
    index -= step;
    }

    //数据位置发生移动,才插入数据
    if (index != i) {
    arr[index] = temp;
    }
    }

    //修改步长
    step /= 2;
    }
    }

6 快速排序

6.1 介绍

  • 快速排序(QuickSort):是对冒泡排序的一种改进
  • 算法思想:选定一个数据作为边界,通过一趟排序将要排序的数据分为两部分,一边小于选定的数据,另一边大于选定的数据。然后对此两组数据,按照上面方法再进行分组。一直递归执行,直到分组的数据小于等于1个时,才退出递归
  • 优点:利用空间来换取时间的典型案例,利用递归来排序,速度快。但因为使用了递归,当数据量庞大的时候,会出现栈溢出的情况(递归本身是使用了栈)

6.2 图解

6.3 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public void quickSort(int[] nums) {
sortByRec(nums, 0, nums.length-1);
}

private void sortByRec(int[] nums, int left_idx, int right_idx) {
if (right_idx - left_idx < 1) {
return;
}
// 创建左右指针,并找出中间节点
int left = left_idx;
int right = right_idx;
int midVal = nums[(left+right)/2];
while (true) {
// 左右值相同时,避免指针不移动
if (nums[left] == nums[right] && nums[left] == midVal) {
if (left != right) {
left ++;
} else {
break;
}
}
// 左指针找出大于中间点的值
while (nums[left] < midVal) {
left++;
}
// 右指针找出小于中间点的值
while (nums[right] > midVal) {
right--;
}
// 左右指针数据交换
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
// 递归
sortByRec(nums, left_idx, left-1);
sortByRec(nums, right+1, right_idx);
}

7 归并排序

7.1 介绍

  • 归并排序(Merge Sort):利用归并的思想实现的排序算法,采用经典的分治策略(分:将问题分成一个小的问题然后递归求解;治:将分的阶段得到的答案结合在一起)
  • 算法思路:将数组的数组递归细分,分到只为1时。再对两个数组按大小顺序加入到一个缓存数组temp,再将缓存数组temp的数据改写进原数组数据
  • 优点:和快速排序一样,引入了递归,牺牲空间来换取时间

7.2 图解

7.3 代码

  • (1)代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    public class MergeSort {

    /**
    * 归并排序 - 升序
    * @param arr 待排序数组
    */
    public static void asc(int[] arr) {
    divideConquer(arr, 0, arr.length-1);
    }


    /**
    * '分 + 治' : 将数组递归分割,并按顺序合并
    * @param arr 待排序原数组
    * @param left 左边界下标
    * @param right 右边界下标
    */
    private static void divideConquer(int[] arr, int left, int right) {
    int mid = (left + right) / 2;

    //当左右边界不重合,可以继续分
    if (left < right) {
    //对左右两边继续进行分
    divideConquer(arr, left, mid);
    divideConquer(arr, mid+1, right);

    //分完再进行'治'
    conquer(arr, left, mid, right);
    }

    }


    /**
    * '治':将两数组比较按顺序加入暂时变量,并改写原数组
    * @param arr 待排序序列
    * @param left 左边界下标
    * @param mid 中间下标(用于将一个数组分割为两个数组)
    * @param right 右边界下标
    */
    private static void conquer(int[] arr, int left, int mid, int right) {

    int lIndex = left; //左边数组头下标
    int rIndex = mid + 1; //右边数组头下标

    int[] temp = new int[right - left + 1]; //暂时变量
    int tIndex = 0; //temp数组下标

    //只要下标没有移动到尾部,一直比较两数组大小
    while(lIndex <= mid && rIndex <= right) {
    //比较两个数组的头下标的大小,谁小谁加入temp
    if (arr[lIndex] < arr[rIndex]) {
    temp[tIndex] = arr[lIndex];
    tIndex ++;
    lIndex ++;
    }else {
    temp[tIndex] = arr[rIndex];
    tIndex ++;
    rIndex ++;
    }
    }

    //将剩余数据加入到temp数组
    //左边数组有剩
    while (lIndex <= mid) {
    temp[tIndex] = arr[lIndex];
    tIndex ++;
    lIndex ++;
    }
    //右边数组有剩
    while (rIndex <= right) {
    temp[tIndex] = arr[rIndex];
    tIndex ++;
    rIndex ++;
    }

    //改写arr数组数据
    tIndex = 0;
    for (int i=left; i <= right; i++) {
    arr[i] = temp[tIndex];
    tIndex ++;
    }
    }
    }
  • (2)测试代码
    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    public void mergeSortTest() {
    int[] arr = new int[]{8, 4, 5, 7, 1, 3, 6, 2};

    MergeSort.asc(arr);

    System.out.println(Arrays.toString(arr));
    }

8 基数排序

8.1 介绍

  • 基数排序(Radix Sort):属于“分配式排序(Distribution Sort)”,又称为“桶子排序(bucket Sort)”。顾名思义,它通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的效果
  • 基数排序是属于稳定性的排序,基数排序是效率高的稳定性排序法
  • 基数排序是桶排序的拓展
  • 实现方法,将整数按位数切割成不同的数字,然后按每个位数分别比较
  • 算法思路:创建10个桶(负数19个),分别代表0, 1, 2, 3, 4, 5, 6, 7, 8, 9,对数组数据求个位数,将数据放入对应个位数的桶中,遍历完毕后按照顺序取出桶中数据放回数组中。下次就取十位数,下下次取百位数,以此类推,直到达到数据中最大位数为止

8.2 图解

8.3 代码

  • 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    public class RadixSort {

    /**
    * 基数排序 - 升序
    * @param arr 待排序序列
    */
    public static void asc(int[] arr) {
    int n = arr.length; //数组长度
    int index; //数组下标

    int[][] buckets = new int[19][n]; //定义桶(算上负数19个桶)
    int[] count = new int[19]; //记录桶中数据个数

    int digit = 1; //位数倍数:个位数1,十位数10,百位数100,以次类推
    int temp; //暂时变量

    //遍历数组求出最大位数
    int max = 0;
    for (int data : arr) {
    //将数据取绝对值,转为字符串,求长度可得位数
    int length = String.valueOf(Math.abs(data)).length();
    if (length > max) {
    max = length;
    }
    }

    int times = 0; //记录循环次数
    while (times < max) {
    //遍历数组存放数据
    for (int data : arr) {
    temp = data / digit % 10; //求位数
    buckets[temp + 9][count[temp + 9]] = data; // +9为了跳过负数的9个桶,-9+9=0就放在0号位桶
    count[temp + 9] ++; //桶中数据个数 + 1
    }

    //取出桶中数据放入原数组
    index = 0;
    for (int i=0; i<buckets.length; i++) {
    //桶中数据不为0,取出数据覆盖原数组
    if (count[i] > 0) {
    for (int j=0; j<count[i]; j++) {
    arr[index] = buckets[i][j];
    index ++;
    }
    count[i] = 0; //数据个数清0
    }
    }

    digit *= 10; //修改位数倍数
    times ++; //次数+1
    }
    }
    }
  • (2)测试
    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    public void radixSortTest() {
    int[] arr = new int[]{132, 125, 43, -51, -72, -115};

    RadixSort.asc(arr);

    System.out.println(Arrays.toString(arr));
    }

9 堆排序

9.1 介绍

  • 前提:堆排序的使用时需要数据结构-树的基础知识建议学完树形结构再来学习堆排序
  • :是具有一定性质的完全二叉树。
  • (1)每个节点的值都大于或等于其左右孩子节点的值,称为大顶堆
  • (2)每个节点的值都小于或等于其左右孩子节点的值,称为小顶堆
  • 堆排序:利用堆来设计的一种排序算法,堆排序是一种选择排序(逐步选择最大/最小值进行处理)。最坏、最好、平均时间复杂度为O(nlogn),属于不稳定排序。升序使用大顶堆,降序使用小顶堆

9.2 思路

  • (1)将待排序的序列构建成一个大顶堆(顺序存储二叉树)
    • 形成大顶堆规则
    • 1、父节点跟子节点比较时从最后的父节点开始比较,逐步往前移
    • 2、如果父节点与子节点发生交换后,判断交换后的子节点是否是父节点,如果是父节点,还要继续跟其子节点进行判断(因为会出现从顶部换一个很小的数给其子节点,而该子节点也是一个父节点,导致父节点<子节点)
  • (2)形成大顶堆后,顶部的根节点就是一个数组的最大值,将其与末尾元素进行交换,此时最大值已经确定了
  • (3)对剩余的n-1个树重复上面操作(构建大顶堆,交换最大值)

9.3 代码

  • (1)代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    public class HeapSort {

    /**
    * 堆排序 - 升序
    * @param arr 待排序数组
    */
    public static void asc(int[] arr) {
    int arrLength = arr.length; //数组长度
    int temp; //中间变量

    //当数组比较个数 <= 1结束循环
    while (arrLength > 1) {
    //从最后一个父节点往前遍历(最后一个父节点为 n/2-1,前面的节点都是父节点)
    for(int i = arrLength/2-1; i >= 0; i --) {
    //实现父节点 > 子节点
    toLargeTop(arr, i, arrLength);
    }

    //形成大顶堆后,根节点和数组末尾交换数据
    temp = arr[arrLength-1];
    arr[arrLength-1] = arr[0];
    arr[0] = temp;

    //数组长度减一,排除最后已确定的数据
    arrLength--;
    }
    }

    /**
    * 从上到下实现 父节点 > 子节点
    * @param arr 待排序数组
    * @param parentIndex 父节点下标
    * @param arrLength 数组长度
    */
    public static void toLargeTop(int[] arr, int parentIndex, int arrLength) {

    int temp; //记录父节点大小
    int childIndex = parentIndex * 2 + 1; //计算左子节点下标

    while (childIndex < arrLength) {
    temp = arr[parentIndex];

    //存在右子节点 且 右子节点>左子节点
    if (childIndex + 1 < arrLength && arr[childIndex] < arr[childIndex + 1]) {
    childIndex++;
    }

    //比较子节点与父节点大小
    if (arr[childIndex] > arr[parentIndex]) {
    arr[parentIndex] = arr[childIndex];
    arr[childIndex] = temp;
    parentIndex = childIndex; //发生交换,父节点下标往下移,比较后面的子节点是否也要发生变化
    }else {
    //没发生交换就退出循环
    break;
    }

    //重新计算子节点下标
    childIndex = parentIndex * 2 + 1;
    }
    }
    }
  • (2)测试
    1
    2
    3
    4
    5
    6
    7
    @Test
    public void heapSortTest() {
    int[] arr = new int[]{1, 3, 20 ,11, 5};

    HeapSort.asc(arr);
    System.out.println(Arrays.toString(arr));
    }