(全)数据结构各种排序的总结(Java实现)
今天整理了一下数据结构课上所学的各种排序,先写一下今天刚敲完的代码(都只写了升序),也是对自己学习的总结,希望能帮到大家。才疏学浅,若有错误希望大家多多指出,一定虚心接受。
首先贴上代码中用到的简单交换函数swap的代码,作用就是交换数组中两个不同位置的数。
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
1.冒泡排序及其优化
思想:冒泡排序是将数组中相邻的两个元素不断比较,第一个比第二个大就交换,因此会将较大的数一直换到最后,然后再缩小比较范围,直至为1排序完成,是一个稳定的排序方法。优化的冒泡排序是增加了一个用于判断数组此时是否有序的变量,有序之后则停止循环。
/*
* 冒泡排序及其优化版本 O(n*n)
*/
public static void bubbleSort(int[] arr) {
int length = arr.length;
if (arr == null || length < 2) {
return;
}
for (int end = length - 1; end > 0; end--) { // 将最大的数依次交换到end位置
for (int j = 0; j < end; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
}
public static void bubbleSortPro(int[] arr) { // 时间复杂度与数据状态有关
int length = arr.length;
if (arr == null || length < 2) {
return;
}
boolean isRandom = true;// 记录此时的数组是否仍是无序的
for (int end = length - 1; end > 0 && isRandom; end--) { // 已经有序后就退出
isRandom = false;
for (int j = 0; j < end; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
isRandom = true;
}
}
}
}
2.插入排序
思想:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止,是一个稳定的排序方法。
/*
* 插入排序 O(n*n)
*/
public static void insertionSort(int[] arr) {
int length = arr.length;
if (arr == null || length < 2) {
return;
}
for (int i = 1; i < length; i++) {// 将第i位上的数当作要插入的数
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {// 依次向前比较 找到大于前面数且小于后面数的位置停下
swap(arr, j, j + 1);
}
}
}
3.选择排序
思想:每一次从待排序的数据元素中选出最小的一个元素,存放在序列的起始位置,然后起始位置向后移,直至待排序列只有一个,这是一个不稳定的排序。
public static void selectionSort(int[] arr) {
int length = arr.length;
if (arr == null || length < 2) {
return;
}
int minIndex = 0;
for (int i = 0; i < length - 1; i++) {
minIndex = i;
for (int j = i + 1; j < length; j++) {
minIndex = arr[j] < arr[minIndex] ? j : minIndex; // 依次找出最小值放在i位置上
}
swap(arr, minIndex, i);
}
}
4.堆排序
思想:堆是一个近似完全二叉树的结构,这里用到的是大根堆,即子结点的键值或索引总是小于它的父节点,再不断取出大根堆的根节点(最大值)就可以实现排序,这是个不稳定的排序。
/*
* 堆排序 O(nlogn) 首先实现两个功能 heapInsert 和 heapify
*/
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2); // 如果插入的节点的值大于父节点,就向上交换
index = (index - 1) / 2;
}
}
public static void heapify(int[] arr, int index, int size) {
int left = index * 2 + 1;
while (left < size) {
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left; // 右孩子不越界的情况下找到左右孩子较大的那个的下标
largest = arr[index] > arr[largest] ? index : largest; // 找出父亲节点和左右孩子中最大的节点的下标
if (index == largest) {
break; // 如果此时父亲节点最大,不用再调整
}
swap(arr, index, largest); // 父亲节点不是最大的话就往下沉,与最大的孩子节点交换
index = largest;
left = index * 2 + 1; // 继续下沉,直至成为大根堆
}
}
public static void heapSort(int[] arr) {
int length = arr.length;
if (arr == null || length < 2) {
return;
}
for (int i = 0; i < length; i++) {
heapInsert(arr, i); // 形成大根堆
}
swap(arr, 0, --length); // 将最大的数交换到最后
while (length > 0) {
heapify(arr, 0, length);// 再次形成大根堆,将此时最大的数取出交换到--length处
swap(arr, 0, --length);
}
}
5.快速排序
思想:通过一趟排序将要排序的数据分割成独立的三部分,小于最后一个数的部分,等于最后一个数的部分,大于最后一个数的部分(参考荷兰国旗问题),然后再按此方法对不等于的两部分进行排序,不是稳定的排序方法。
/*
* 快速排序 O(nlogn) 这里写的是随机快排
*/
public static void quickSort(int[] arr) {
int length = arr.length;
if (arr == null || length < 2) {
return;
}
quickSort(arr, 0, length - 1);
}
public static void quickSort(int[] arr, int l, int r) {
if (l < r) {
swap(arr, l + (int) (Math.random() * (r - l + 1)), r); // 随机快排比经典快排只多这一步
int[] p = partition(arr, l, r);
quickSort(arr, l, p[0] - 1);
quickSort(arr, p[1] + 1, r);
}
}
public static int[] partition(int[] arr, int l, int r) {
int less = l - 1;
int more = r;
int index = l;
while (index < more) {
if (arr[index] < arr[r]) {
swap(arr, index++, ++less); // 遇到比最后一个数小的数就放在less区域后的第一个位置(即等于区域的第一个),less再++扩充范围,此时index也++比较下一个
} else if (arr[index] > arr[r]) {
swap(arr, index, --more);// 遇到比最后一个数大的数就放在more区域,此时交换过来的数并没有参与比较,所以index不++
} else {
index++;// 相等的话index++
}
}
swap(arr, more, r);// 将最后一个的位置调整好
return new int[] { less + 1, more }; // 返回相等区间的范围
}
6.归并排序
思想:采用分治的一个非常典型的应用。首先将所要排序的序列一分为二递归进行,再将已有序的子序列合并,得到完全有序的序列,是个稳定的排序方法。
/*
* 归并排序 O(nlogn)
*/
public static void mergeSort(int[] arr) {
int length = arr.length;
if (arr == null || length < 2) {
return;
}
mergeSort(arr, 0, length - 1);
}
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {
return;
}
int mid = l + (r - l) / 2;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
public static void merge(int[] arr, int l, int mid, int r) {
int[] help = new int[r - l + 1];
int p1 = l;
int p2 = mid + 1;
int index = 0;
while (p1 <= mid && p2 <= r) {
help[index++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[index++] = arr[p1++];
}
while (p2 <= r) {
help[index++] = arr[p2++];
}
for (int i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
7.桶排序
思想:将数组分到有限数量的桶里,再按顺序将每个桶中的数据取出,所以桶排序不是基于比较的排序。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n)),是一种稳定的排序方法,但是当数据量较大时会使用较大的空间,因此更适用于数据量小的时候。
/*
* 桶排序
*/
public static void bucketSort(int[] arr) {
int length = arr.length;
if (arr == null || length < 2) {
return;
}
int maxValue = Integer.MIN_VALUE;
for (int i = 0; i < length; i++) {
maxValue = arr[i] > maxValue ? arr[i] : maxValue; // 找出数组中的最大值
}
int bucket[] = new int[maxValue + 1];
for (int i = 0; i < length; i++) {
bucket[arr[i]]++; // 依次放进对应的桶中
}
int index = 0;
for (int i = 0; i <= maxValue; i++) {
while (bucket[i]-- > 0) {
arr[index++] = i;// 从桶中取出
}
}
}
8.基数排序
思想:属于“分配式排序”,通俗来讲就是先按照个位数值项数组排好序,在个位排好序的基础上之后再通过比较十位数值排序,以此类推直到把最高位的顺序也排好,是一种稳定的排序方法。
/*
* 基数排序 (写了两种方法,个人感觉可能第二种更好理解)
*/
public static void radixSort(int[] arr) {
int length = arr.length;
if (arr == null || length < 2) {
return;
}
final int radix = 10;
int bucket[] = new int[length];
int maxDigit = getMaxDigit(arr);
for (int i = 1; i <= maxDigit; i++) {
int count[] = new int[radix];
for (int j = 0; j < length; j++) {
count[getRemainder(arr[j], i)]++; // 记录各个位上数值为0~9的个数有多少
}
for (int j = 1; j < 10; j++) {
count[j] = count[j - 1] + count[j]; // 将count[i]的值转变为在bucket数组中的最大位置的数值
}
for (int j = length - 1; j >= 0; j--) { // 因为count[remainder]是从大到小往bucket数组里放,为了保留上一次排序的顺序,需要倒序遍历
int remainder = getRemainder(arr[j], i);
bucket[--count[remainder]] = arr[j];
}
for (int j = 0; j < length; j++) {
arr[j] = bucket[j]; // 将排序的结果放回数组
}
}
}
public static int getRemainder(int x, int y) { // 返回X在个位、十位...上的数值
return ((x / ((int) Math.pow(10, y - 1))) % 10);
}
public static int getMaxDigit(int[] arr) {// 返回数组中最大数的位数
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
int res = 0;
while (max != 0) {
res++;
max /= 10;
}
return res;
}
第二种方法使用了二维数组来存储bucket的值,额外空间复杂度更高。
public static void radixSortDMArray(int[] arr) {
int length = arr.length;
if (arr == null || length < 2) {
return;
}
final int radix = 10;
int maxDigit = getMaxDigit(arr);
for (int i = 1; i <= maxDigit; i++) {
int[] count = new int[radix];
int[][] bucket = new int[10][length];
for (int j = 0; j < length; j++) {
int remainder = getRemainder(arr[j], i);
bucket[remainder][count[remainder]++] = arr[j];
}
int index = 0;
for (int j = 0; j < 10; j++) {
if (count[j] != 0) {
for (int k = 0; k < count[j]; k++) {
arr[index++] = bucket[j][k];
}
}
}
}
}
9.希尔排序
思想:是直接插入排序算法的一种更高效的改进版本,主要体现在使用增量来分组排好序,再不断缩小增量,最后增量为1时,分组为1,整个数组有序,是个非稳定的排序方法。
/*
* 希尔排序
*/
public static void shellSort(int[] arr) {
int length = arr.length;
if (arr == null || length < 2) {
return;
}
length /= 2; // 增量
while (length >= 1) {
for (int i = length; i < arr.length; i++) {
int temp = arr[i];
int index;
for (index = i - length; index >= 0 && arr[index] > temp; index = index - length) { // 把arr[i]的值在分組中向前直接插入
arr[index + length] = arr[index];
}
arr[index + length] = temp; // index在第一个小于temp的地方停下,所以在index后面的位置插入
}
length /= 2;
}
}
有关稳定性的总结
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。以上的排序中稳定的排序方法有:冒泡排序、插入排序、归并排序、基数排序、桶排序;非稳定的排序方法有:选择排序、快速排序、堆排序、希尔排序。 详细解释请见排序算法稳定性。
才疏学浅,若有错误希望大家多多指出,一定虚心接受。