冒泡排序
最佳情况:T(n) = O(n) 最差情况:T(n) = O(n^2) 平均情况:T(n) = O(n^2)
// 可以利用一个flag,标记是否进行了swap操作,来适当加快排序速度
public static int[] bubbleSort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++) {
boolean flag = true;
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j + 1] < array[j]) {
flag = false;
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
if(flag == true)
return array;
}
return array;
}
选择排序
最佳情况:T(n) = O(n^2) 最差情况:T(n) = O(n^2) 平均情况:T(n) = O(n^2)
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
// 选择排序,第n趟排序后,一定至少有n个数字已经在目标位置上
public static int[] selectionSort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i; j < array.length; j++) {
if (array[j] < array[minIndex]) //找到最小的数
minIndex = j; //将最小数的索引保存
}
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
return array;
}
插入排序
最佳情况:T(n) = O(n) 最坏情况:T(n) = O(n^2) 平均情况:T(n) = O(n^2)
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
// 插入排序,将一个未排序的数字,依次与已经排序好的数进行比较,需要将已排序的数往后挪位
public static int[] insertionSort(int[] array) {
int len = array.length;
// 基本情况下的数组可以直接返回
if(array == null || len == 0 || len == 1) {
return array;
}
for (int i = 0; i < len - 1; i++) {
// 第一个数默认已排序,从第二个数开始
int current = array[i + 1];
// 前一个数的下标
int preIdx = i;
// 拿当前的数与之前已排序序列逐一往前比较,
// 如果比较的数据比当前的大,就把该数往后挪一步
while (preIdx >= 0 && current < array[preIdx]) {
array[preIdx + 1] = array[preIdx];
preIdx--;
}
// while循环跳出说明找到了位置
array[preIdx + 1] = current;
}
return array;
}
希尔排序
最佳情况:T(n) = O(nlog2 n) 最坏情况:T(n) = O(nlog2 n) 平均情况:T(n) =O(nlog2 n)
在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。(同组中的元素采用插入排序)下图中,同一颜色在同一组!
// 希尔排序(其中每组使用了插入排序)
public static int[] ShellSort(int[] array){
int len = array.length;
int gap = len / 2;
while(gap > 0){
for(int i = gap; i < len; i++){
int temp = array[i];
int preIndex = i - gap;
while(preIndex >= 0 && array[preIndex] > temp){
array[preIndex + gap] = array[preIndex];
preIndex -= gap;
}
array[preIndex + gap] = temp;
}
gap /= 2;
}
return array;
}
归并排序
最佳情况:T(n) = O(n) 最差情况:T(n) = O(nlogn) 平均情况:T(n) = O(nlogn)
该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
public static int[] MergeSort(int[] array) {
if (array.length < 2) return array;
int mid = array.length / 2;
int[] left = Arrays.copyOfRange(array, 0, mid);
int[] right = Arrays.copyOfRange(array, mid, array.length);
return merge(MergeSort(left), MergeSort(right));
}
/**
* 归并排序——将两段排序好的数组结合成一个排序数组
*/
public static int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
for (int index = 0, i = 0, j = 0; index < result.length; index++) {
if (i >= left.length)
result[index] = right[j++];
else if (j >= right.length)
result[index] = left[i++];
else if (left[i] > right[j])
result[index] = right[j++];
else
result[index] = left[i++];
}
return result;
}
快速排序
最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)
- 从数列中挑出一个元素,称为 “基准”(pivot)(下例挑选数组的第一个元素作为基准);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
注意partiotion函数中 i 、j 取值,以及while循环判断条件
// 快速排序
public static int[] quickSort(int[] a,int start, int end){
if(start < end){ //至少两个位置
int pivotIndex = partition(a, start, end); // 定义pivotIndex中间位置。partition是检索这个方法
quickSort(a, start ,pivotIndex - 1); //排序左半边
quickSort(a,pivotIndex + 1, end); //排序右半边
}
return a;
}
public static int partition(int[] a, int start, int end){
//对数组A下标从start到end中选一个主元,确定其位置,左边小于,右边大于。
int pivot = a[start]; //先定义区间数组第一个元素为主元
int i = start; //定义最低的索引i是start。比主元大一位
int j = end; //定义最高的索引j是end
while(i < j){ //当不等于high的位置时,执行以下循环
while(a[j] > pivot && i < j) //当 j的索引上的值比主元大时,且索引大于i时
j--; //寻找比主元小的值的位置索引
while(a[i] <= pivot && i < j) //当i的索引上的值比主元小时,索引小于j时!! 注意等号!!!
i++; //寻找比主元大的值的位置索引。
if(i < j){ //交换low和high的值
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
a[start] = a[j];
a[j] = pivot;
return j;
}
堆排序
为什么选择快速排序而不选择堆排序
-
堆排序访问数据的方式没有快速排序友好
-
对于同样的数据,在排序过程中,堆排序的数据交换次数要多于快速排序
最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(nlogn) 平均情况:T(n) = O(nlogn)
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
**(下标从0开始(0 ~ n-1))**完全二叉树序号为 i 的节点,其左子树序号为 2*i+1 (如果存在),右子数序号为 2*i+2
完全二叉树一共有n个节点,则最后一个非叶子节点的下标为 n/2 - 1 ,下标从0开始(0 ~ n-1)
// 堆排序
public static int[] HeapSort(int[] array) {
int len = array.length;
if (len < 1) return array;
//1.构建一个最大堆
//从最后一个非叶子节点开始向上构造最大堆
for (int i = (len/2 - 1); i >= 0; i--) { //此处应该为 i = (len/2 - 1)
adjustHeap(array, i, len);
}
//2.循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
while (len > 0) {
int temp = array[len-1];
array[len-1] = array[0];
array[0] = temp;
len--;
adjustHeap(array, 0, len);
}
return array;
}
// 调整使之成为最大堆
public static void adjustHeap(int[] array, int i, int len) {
int maxIndex = i;
//如果有左子树,且左子树大于父节点,则将最大指针指向左子树
if (i * 2 + 1 < len && array[i * 2 + 1] > array[maxIndex])
maxIndex = i * 2 + 1;
//如果有右子树,且右子树大于父节点,则将最大指针指向右子树
if (i * 2 + 2 < len && array[i * 2 + 2] > array[maxIndex])
maxIndex = i * 2 + 2;
//如果父节点不是最大值,则将父节点与最大值交换
if (maxIndex != i) {
int temp = array[i];
array[i] = array[maxIndex];
array[maxIndex] = temp;
// 只要调整了某个节点,就需要重新将被调整节点下面再调整为大顶堆
adjustHeap(array,maxIndex,len);
}
}
计数排序
最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n+k)
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
// 计数排序
public static int[] CountingSort(int[] array) {
if (array.length == 0) return array;
int min = array[0];
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max)
max = array[i];
if (array[i] < min)
min = array[i];
}
int bias = 0 - min;
int[] bucket = new int[max - min + 1];
Arrays.fill(bucket, 0);
for (int i = 0; i < array.length; i++) {
bucket[array[i] + bias]++;
}
int index = 0;
int i = 0; // 标记桶的下标
while (index < array.length) {
if (bucket[i] != 0) { // 当此桶还有元素
array[index] = i - bias;
bucket[i]--; // 出桶
index++;
} else
i++;
}
return array;
}
桶排序
最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n2)
和计数排序类似,桶排序也对输入数据作了某种假设,因此它的速度也很快。具体来说,计数排序假设了输入数据都属于一个小区间内的整数,而桶排序则假设输入数据是均匀分布的,即落入每个桶中的元素数量理论也是差不多的,不会出现很多数落入同一个桶内的情况。
桶排序中很重要的一步就是桶的设定了,我们必须根据输入元素的情况,选择一个恰当的 “ getBucketIndex ” 算法,使得输入元素能够正确的放入对应的桶内,且保证输入数据能够尽量均匀的放入不同的桶内。
最糟糕的情况下,即所有的数据都放入了一个桶内,桶内自排序算法为插入排序,那么其时间复杂度就为 O(n ^ 2) 了。
其次,我们可以发现,区间划分的越细,即桶的数量越多,理论上分到每个桶中的元素就越少,桶内数据的排序就越简单,其时间复杂度就越接近于线性。
极端情况下,就是区间小到只有1,即桶内只存放一种元素,桶内的元素不再需要排序,因为它们都是相同的元素,这时桶排序差不多就和计数排序一样了。
// 桶排序
public static float[] bucketSort(float[] arr) {
// 新建一个桶的集合
ArrayList<LinkedList<Float>> buckets = new ArrayList<LinkedList<Float>>();
for (int i = 0; i < 10; i++) {
// 新建一个桶,并将其添加到桶的集合中去。
// 由于桶内元素会频繁的插入,所以选择 LinkedList 作为桶的数据结构
buckets.add(new LinkedList<Float>());
}
// 将输入数据全部放入桶中并完成排序
for (float data : arr) {
int index = getBucketIndex(data);
insertSort(buckets.get(index), data);
}
// 将桶中元素全部取出来并放入 arr 中输出
int index = 0;
for (LinkedList<Float> bucket : buckets) {
for (Float data : bucket) {
arr[index++] = data;
}
}
return arr;
}
/**
* 计算得到输入元素应该放到哪个桶内
*/
public static int getBucketIndex(float data) {
// 这里例子写的比较简单,仅使用浮点数的整数部分作为其桶的索引值
// 实际开发中需要根据场景具体设计
return (int) data;
}
/**
* 我们选择插入排序作为桶内元素排序的方法 每当有一个新元素到来时,我们都调用该方法将其插入到恰当的位置
*/
public static void insertSort(List<Float> bucket, float data) {
ListIterator<Float> it = bucket.listIterator();
while (it.hasNext()) {
if (data <= it.next()) {
it.previous(); // 把迭代器的位置偏移回上一个位置
it.add(data); // 把数据插入到迭代器的当前位置
return; // 已插入数据,直接退出函数
}
}
it.add(data); // 否则把数据插入到链表末端
}
基数排序
最佳情况:T(n) = O(n * k) 最差情况:T(n) = O(n * k) 平均情况:T(n) = O(n * k)
类似于我们小时候比较数,会先从个数比,再比较十位,再比较百位…(没有则取0)
- 取得数组中的最大数,取得其位数(最外层循环次数);
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
// 基数排序
public static int[] RadixSort(int[] array) {
if (array == null || array.length < 2)
return array;
// 1.先算出最大数的位数;
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max, array[i]);
}
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
for (int j = 0; j < array.length; j++) {
int num = (array[j] % mod) / div;
bucketList.get(num).add(array[j]);
}
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
array[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return array;
}
基数排序 vs 计数排序 vs 桶排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
- 基数排序:根据键值的每位数字来分配桶
- 计数排序:每个桶只存储单一键值
- 桶排序:每个桶存储一定范围的数值
术语以性能比较
-
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
-
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
-
内排序:所有排序操作都在内存中完成;
-
外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
-
时间复杂度: 一个算法执行所耗费的时间。
-
空间复杂度:运行完一个程序所需内存的大小。
-
n: 数据规模
-
k: “桶”的个数
-
In-place: 占用常数内存,不占用额外内存
-
Out-place: 占用额外内存
比较排序和非比较排序的区别
常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logn次,所以时间复杂度平均O(nlogn)。
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
比较排序和非比较排序的区别
常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。
在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logn次,所以时间复杂度平均O(nlogn)。
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。
计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。
非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。