冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
public int[] BubbleSort(int[] nums) {
int len = nums.length;
for (int i = 0; i < len; i++) {
boolean flag = true;
for (int j = 0; j < len - i - 1; j++) {
if (nums[j] > nums[j + 1]) {
flag = false;
int temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
if (flag) {
break;
}
}
return nums;
}
选择排序是一种简单直观的排序算法,它的工作原理是每一次从待排序的数据元素中选出最小(最大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到全部待排序的数据元素排完。选择排序是不稳定的排序方法。
public int[] SelectionSort(int[] nums) {
int len = nums.length;
for (int i = 0; i < len; i++) {
int index = i;
for (int j = i + 1; j < len; j++) {
if (nums[index] > nums[j]) {
index = j;
}
}
if (index != i) {
int temp = nums[i];
nums[i] = nums[index];
nums[index] = temp;
}
}
return nums;
}
对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入,直到插完所有元素为止。
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。所以在数据量小数据基本有序的情况下用插入排序最优。
public int[] InsertSort(int[] nums) {
int len = nums.length;
//i表示前i个数字为一个有序序列
for (int i = 1; i < len; i++) {
//记录第i个数字(要插入有序序列,边比较,边移位,即将第i个位置视为temp)
int temp = nums[i];
int j = i - 1;
//遍历有序序列,直到到达有序序列边界或者找到比自己小的序列为止
while (j >= 0 && temp < nums[j]) {
//在找的过程中边移位
nums[j + 1] = nums[j];
j--;
}
//到达边界,或者找到比自己小的数字的位置
nums[j + 1] = temp;
}
return nums;
}
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
public int[] shellSort(int[] nums) {
int len = nums.length;
for (int step = len / 2; step >= 1; step /= 2) {
//做法与插入排序相似,只是步数从1变为step,然后慢慢减为1
//这样做的目的是先让数组相对有序,然后再进行插入排序。
for (int i = step; i < len; i++) {
int temp = nums[i];
int j = i - step;
while (j >= 0 && temp < nums[j]) {
nums[j + step] = nums[j];
j -= step;
}
nums[j + step] = temp;
}
}
return nums;
}
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
1.申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
4. 重复步骤 3 直到某一指针达到序列尾;
5. 将另一序列剩下的所有元素直接复制到合并序列尾。
//在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
private int[] temp;
public int[] MergeSort(int[] nums) {
int len = nums.length;
temp = new int[len];
Sort(0, len - 1, nums);
return nums;
}
private void Sort(int left, int right, int[] arr) {
if (left < right) {
int mid = (left + right) / 2;
Sort(left, mid, arr);//左边归并排序,使左边有序
Sort(mid + 1, right, arr);//右边归并排序,使右边有序
margin(left, mid, right, arr);//将左右两边合并成一个有序数组
}
}
private void margin(int left, int mid, int right, int[] arr) {
//排序left指向左边有序数组中最小的元素
//mid + 1指向右边有序数组中最小的元素
int x = left;
int y = mid + 1;
int index = left;
//当左边右边都还有元素时,那个小那个先放入temp中
while (x <= mid && y <= right) {
if (arr[x] < arr[y]) {
temp[index++] = arr[x++];
} else {
temp[index++] = arr[y++];
}
}
//若左边还剩元素,则将左边元素放入temp中
while (x <= mid) {
temp[index++] = arr[x++];
}
//若右边还剩元素,则将左边元素放入temp中
while (y <= right) {
temp[index++] = arr[y++];
}
//将temp中的元素放回arr中
for (int i = left; i < right + 1; i++) {
arr[i] = temp[i];
}
}
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
快速排序简介
public int[] QuickSort(int[] nums) {
int len = nums.length;
Sort2(0, len - 1, nums);
return nums;
}
private void Sort2(int left, int right, int[] arr) {
if (left < right) {
int partition = getPartition(left, right, arr);
Sort2(left, partition - 1, arr);
Sort2(partition + 1, right, arr);
}
}
private int getPartition(int left, int right, int[] arr) {
//以最右边的元素为基准值
int pivot = right;
while (left < right) {
//找到比pivot更大的值
while (left < right) {
if (arr[left] > arr[pivot]) {
break;
}
left++;
}
//找到比pivot更小的值
while (left < right) {
if (arr[right] < arr[pivot]) {
break;
}
right--;
}
Swap(left, right, arr);
}
Swap(left, pivot, arr);
return right;
}
private void Swap(int x, int y, int[] arr) {
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:
堆排序的平均时间复杂度为 Ο(nlogn)。
大顶堆:
public int[] HeapSort_1(int[] nums) {
int len = nums.length;
//按照二叉树的定义,第一个非叶子节点位置在len/2-1处,每次--即可遍历所有非叶子节点,知道遍历到根节点
for (int i = len / 2 - 1; i >= 0; i--) {
heapify_1(nums, i, len);
}
for (int i = len - 1; i > 0; i--) {
Swap(i, 0, nums);
len = len - 1;
heapify_1(nums, 0, len);
}
return nums;
}
/**
* 堆调整,使得根节点的值最大
*
* @param arr:堆
* @param root:根节点
*/
private void heapify_1(int[] arr, int root, int len) {
//按照二叉树的性质,左边叶子节点为根节点位置的两倍加1,右边叶子节点为根节点的2倍加2
int left = root * 2 + 1;
int right = root * 2 + 2;
int largest = root;
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != root) {
Swap(largest, root, arr);
heapify_1(arr, largest, len);
}
}
private void Swap(int x, int y, int[] arr) {
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
小顶堆
public int[] HeapSort_2(int[] nums) {
int len = nums.length;
//按照二叉树的定义,第一个非叶子节点位置在len/2-1处,每次--即可遍历所有非叶子节点,知道遍历到根节点
for (int i = len / 2 - 1; i >= 0; i--) {
heapify_2(nums, i, len);
}
for (int i = len - 1; i > 0; i--) {
Swap(i, 0, nums);
len = len - 1;
heapify_2(nums, 0, len);
}
return nums;
}
/**
* 堆调整,使得根节点的值最小
*
* @param arr:堆
* @param root:根节点
*/
private void heapify_2(int[] arr, int root, int len) {
//按照二叉树的性质,左边叶子节点为根节点位置的两倍加1,右边叶子节点为根节点的2倍加2
int left = root * 2 + 1;
int right = root * 2 + 2;
int largest = root;
if (left < len && arr[left] < arr[largest]) {
largest = left;
}
if (right < len && arr[right] < arr[largest]) {
largest = right;
}
if (largest != root) {
Swap(largest, root, arr);
heapify_2(arr, largest, len);
}
}
private void Swap(int x, int y, int[] arr) {
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序的特征
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
public void CountingSort(int[] nums) {
int min = nums[0];
int max = nums[0];
for (int i = 1; i < nums.length; i++) {
if (nums[i] < min) {
min = nums[i];
continue;
}
if (nums[i] > max) {
max = nums[i];
}
}
int[] maxAndMin = new int[max - min + 1];
for (int i : nums) {
maxAndMin[i - min]++;
}
int index = 0;
for (int i = 0; i < maxAndMin.length; i++) {
int num = maxAndMin[i];
while (num > 0) {
nums[index++] = i + min;
num--;
}
}
}