排序是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为“有序”的记录序列。排序分为内部排序和外部排序。
若整个排序过程不需要访问外存便能完成,责成此类排序问题为内部排序。反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。
如果按照策略分类,可分为:交换排序、插入排序、选择排序、归并排序。其中,冒泡排序和快速排序属于交换排序;直接插入排序和希尔排序属于插入排序;简单选择排序和堆排序属于选择排序。
- 稳定排序:冒泡排序、直接插入排序、归并排序。
- 不稳定排序:快速排序、希尔排序、选择排序、快速排序。
一、冒泡排序(Bubble Sort)
(1)算法思想:
重复地走访要排序的数列,一次比较两个数据元素,如果顺序不对则进行交换,并一直重复这样的走访操作,直到没有要交换的数据元素为止。
(2)代码:
public class BubbleSort {
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
public static void main(String[] args) {
int[] arr = {64, 34, 25, 12, 22, 11, 90};
bubbleSort(arr);
for (int num : arr) {
System.out.print(num + " ");
}
}
}
(3)复杂度:
时间复杂度为O(n^2)。
(4)使用情况:
不适用于大规模数据集。
(5)优化
- 改进(1):如果某次冒泡不存在数据交换,这说明已经有序,可以直接推出排序。
- 改进(2):头尾进行冒泡,每次把最大的沉底,最小的浮上去,两边往中间靠。
二、插入排序(Insertion Sort)
(1)算法思想:
将数组分为已排序和未排序两部分,每次将一个待排序的元素,插入到前面已经排好序的部分适当位置,直到全部元素插入完毕。
(2)实现:
(3)代码:
public class InsertionSort {
public static void insertionSort(int[] arr) {
int n = arr.length;
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
}
public static void main(String[] args) {
int[] arr = {12, 11, 13, 5, 6};
insertionSort(arr);
for (int num : arr) {
System.out.print(num + " ");
}
}
}
例如,对于数组 [12, 11, 13, 5, 6]
,排序过程如下:
首先,从第二个元素 11
开始,将其与前面的 12
比较,由于 11 < 12
,将 12
后移,11
插入到正确位置,此时数组变为 [11, 12, 13, 5, 6]
。
接着,处理第三个元素 13
,由于它大于前面已排序的 11
和 12
,无需移动,数组仍为 [11, 12, 13, 5, 6]
。
然后,处理第四个元素 5
,它依次与 13
、 12
、 11
比较并移动,最终插入到正确位置,数组变为 [5, 11, 12, 13, 6]
。
最后,处理第五个元素 6
,经过比较和移动,数组最终排序为 [5, 6, 11, 12, 13]
。
(4)复杂度
直接插入排序在小型数据集上表现较好,其平均时间复杂度为 O(n^2),空间复杂度为 O(1) 。
(5)使用情况
对于小型数据集和基本有序的数组性能较好。
三、直接插入排序
将第一个元素和第二个元素排好序,然后将第三个元素插入到已经排好序的元素中,以此类推。数组已经有序就是插入排序最好的情况。
(1)算法思想:
将数组中的所有元素跟前面已经排好的元素进行比较,如果选择的元素比已经排序的元素小,则交换,直到全部元素都比较过。
(2)实现:
两层循环,第一层循环遍历待比较的所有数组元素。第二层循环将本轮选择的元素(a)和已经排好序的元素(b)陆续进行比较。如果a > b,二者交换。
(3)代码:
public static void insertSort(int[] array) {
int temp;
int j;
for (int i = 1; i < array.length; i++) {//从1号位置开始排序
temp = array[i];//将待排序元素保存
j = i;//保存元素下表
//当前元素和前一个元素j - 1进行比较,当当前元素<j- 1号,将j - 1号位置后移,j往前走一下
while(j > 0 && temp < array[j - 1]) {
array[j] = array[j - 1];
j --;
}
array[j] = temp;//将排序元素放入相应位置
}
}
四、选择排序(Selection Sort)
(1)算法思想:
将数组分为已排序和未排序两部分,每次从未排序部分选择最小的元素并将其放入已排序部分。
(2)实现:
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
(3)代码:
public class SelectionSort {
public static void selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
int min_idx = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[min_idx]) {
min_idx = j;
}
}
if (min_idx!= i) {
int temp = arr[i];
arr[i] = arr[min_idx];
arr[min_idx] = temp;
}
}
}
public static void main(String[] args) {
int[] arr = {64, 25, 12, 22, 11};
selectionSort(arr);
for (int num : arr) {
System.out.print(num + " ");
}
}
}
(4)复杂度
时间复杂度为O(n^2)。
(5)使用情况
不适用于大规模数据集。
五、简单选择排序
选出最小的数和第一个数交换,再在剩余的数中又选择最小的和第二个数交换,以此类推。
(1)思想:
比较 + 交换。
1、从待排序序列中,找到关键字最小的元素。
2、如果最小元素不是待排序序列的第一个元素,将其和第一个元素交换。
3、从余下的N -1个元素中,找出关键字最小的元素。重复1、2步,直到排序结束。
(2)实现:
两层循环:第一层循环依次遍历序列当中的每一个元素。第二层循环将遍历得到的当前元素和剩下的元素进行比较,符合最小元素的条件,进行交换。
(3)代码:
public static void selectSort(int[] array) {
int temp;
int k;
for (int i = 0; i < array.length - 1; i++) {
k = i;
for (int j = i + 1; j < array.length; j++) {
if (array[j] < array[k]) {
k = j;//k指向最小元素
}
}
temp = array[i];
array[i] = array[k];
array[k] = temp;
}
}
六、快速排序(Quick Sort)
(1)算法思想
选择一个基准元素,将数组分成两部分,左边的元素小于基准,右边的元素大于基准。通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的关键字均比另一部分关键字小,然后分别对这两部分记录继续进行排序,以达到整个序列有序。
选择一个基准元素,比基准元素小的放在基准元素的前面,比基准元素大的放基准元素的后面。这种动作叫做分区,每次分区都把一个数列分成了两部分,每次分区都使得一个数字有序。然后将基准元素前面部分和后面部分继续分区,一直分区,直到分区的区间中只有一个元素的时候,一个元素的序列肯定是有序的,此时排序完成。
(2)实现
(3)代码
public class QuickSort {
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
public static int partition(int[] arr, int low, int high) {
int pivot = arr[high];
int i = (low - 1);
for (int j = low; j <= high - 1; j++) {
if (arr[j] <= pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return (i + 1);
}
public static void main(String[] args) {
int[] arr = {10, 7, 8, 9, 1, 5};
int n = arr.length;
quickSort(arr, 0, n - 1);
for (int num : arr) {
System.out.print(num + " ");
}
}
}
(4)复杂度
平均时间复杂度为O(n log n)。
(5)使用情况
性能较好。
七、归并排序(Merge Sort)
(1)算法思想:
将待排序的序列分成若干个子序列,每个子序列有序,然后将子序列合并成一个完整的有序序列。
(2)实现
(3)代码
public class MergeSort {
public static void mergeSort(int[] arr, int l, int r) {
if (l < r) {
int m = l + (r - l) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}
}
public static void merge(int[] arr, int l, int m, int r) {
int n1 = m - l + 1;
int n2 = r - m;
int[] L = new int[n1];
int[] R = new int[n2];
for (int i = 0; i < n1; i++) {
L[i] = arr[l + i];
}
for (int j = 0; j < n2; j++) {
R[j] = arr[m + 1 + j];
}
int i = 0, j = 0, k = l;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k++] = L[i++];
} else {
arr[k++] = R[j++];
}
}
while (i < n1) {
arr[k++] = L[i++];
}
while (j < n2) {
arr[k++] = R[j++];
}
}
public static void main(String[] args) {
int[] arr = {12, 11, 13, 5, 6};
mergeSort(arr, 0, arr.length - 1);
for (int num : arr) {
System.out.print(num + " ");
}
}
}
(4)复杂度
时间复杂度为O(n log n)
(5)使用情况
性能稳定。
八、堆排序
堆排序是一种选择排序,它使用二叉堆数据结构来进行排序。它的基本思想是将待排序的数组构建成一个最大堆(或最小堆),然后将堆顶元素与堆底元素交换,从堆中移除最大(或最小)元素,并递减堆的大小,重复这个过程直到堆为空。它的时间复杂度为O(n log n),性能稳定。
(1)算法思想:
以升序排序为例,利用小根堆的性质(堆顶元素最小),不断输出最小元素,直到堆中没有元素。
1、构建小根堆
2、输出堆顶元素
3、将堆底元素放一个到堆顶,再重新构造小根堆,再输出堆顶元素,以此类推。
(1)堆的概念
堆:本质是一种数组现象。任意的叶子节点小于(或大于)它所有的父节点。又分为大根堆和小根堆。大根堆要求节点的元素都要大于其孩子。小根堆要求节点元素都小于其孩子。两者对左右孩子的大小关系不做要求。
(2)基本思想(大根堆):
1、首先将序列构建为大根堆,此时位于根节点的元素一定是该序列的最大值。
2、取出当前大根堆的根节点,将其与序列末尾元素进行交换。此时序列末尾元素为有序的最大值,但是位于根节点的元素并不一定满足大根堆的性质。
3、对交换后的n - 1个序列元素进行调整,使其满足大根堆的性质。
4、重复2、3步骤,直至堆中只有一个元素为止。
(3)算法核心
1、如何将n个待排序的数建成堆?
(1)n个节点的完全二叉树,则最后一个结点是第[n/2]个节点的子树。
(2)筛选从第[n/2]个节点为根的子树开始,该子树成为堆。
(3)之后向前依次对各节点为根的子树进行筛选,是指成为堆,直到根节点。
2、输出堆顶元素后,怎样调整剩余n - 1个元素,使其成为一个新堆?
(1)设有m个元素的堆,输出堆顶元素后,剩下m - 1个元素。将堆底元素送入堆顶。
(2)将根节点与左、右子树中的较小元素进行交换。
(3)若和左子树交换:如果左子树堆被破坏,即左子树的根节点不满足堆的性质,则重复方法(2)。
(4)若和右子树交换:如果右子树堆被破坏,即右子树的根节点不满足对的性质,重复方法(2)。
(5)继续对不满足堆性质的子树进行上述交换操作,直到叶子节点,堆建成。
(2)代码:
public static void heapSort(int[] arr) {
int n = arr.length;
// 构建最大堆
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 依次取出堆顶元素并调整堆
for (int i = n - 1; i > 0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
heapify(arr, i, 0);
}
}
private static void heapify(int[] arr, int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
int swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
heapify(arr, n, largest);
}
}
九、希尔排序
插入排序每次只能操作一个元素,效率低。
元素个数N,取基数k = N/2,将下标差值为k的数分为一组(一组元素的个数看总元素的个数决定),在组内构成有序序列,再取k = k/2,将下标差值为k的数分为一组,构成有序序列,直到k = 1,然后再进行直接插入排序。
希尔排序也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
(1)算法思想:
先将整个待排序的记录序列分割成若干子序列分别进行直接插入排序,待整个序列中的记录基本有序时,再对全体记录进行依次直接插入排序。
(2)实现:
1、选择一个增量序列t1,t2,...,tk,其中ti > tj,tk = 1;
2、按增量序列个数k,对序列进行k趟排序;
3、每趟排序根据对应的增量ti,将待排序序列分割成长度为m的子序列,分别对各字表进行直接插入排序。当增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。
(3)代码:
/**
* 希尔排序
* 最好时间复杂度:O(nlog n)
* 最坏时间复杂度:O(n^2)
* @param array
*/
public static void shellSort(int[] array) {
int h = 1;
int j;
int temp;
while (h < array.length) {
h = 3 * h + 1;
}
while (h > 0) {
for (int i = h; i < array.length; i++) {
j = i;
temp = array[i];
//跨间隔进行插入排序
while (j > h - 1 && temp < array[j - h]) {
array[j] = array[j - h];
j -=h;
}
array[j] = temp;
}
h = (h - 1) / 3;
}
}
十、归并排序
将一个无序的数列一直一分为二,直到分到序列中只有一个数的时候这个序列肯定有序。然后将两个含有一个数字的序列合并为含有两个数字的有序序列,这样一直进行下去,整个数列有序。