以下所有的排序全部是正序。
选择排序
首先找到数组中最小的一个数,然后将该值和第一个元素交换。如果最小的数就是第一个元素本身,自己和自己交换。再剩下的元素中找到第二小的数,和第二个元素交换,如此反复,直到整个数组排序。选择排序就是不断地选择最值。
时间复杂度: O(N^2)
空间复杂度 :O(1)
选择排序运行时间和输入数组无关。不论输入的数组是否有序,选择排序所耗费的时间是固定的。因为每次选择最小值并不能为下一轮选择最值提供什么有用的信息。
‘
选择排序的数据移动最少。选择排序只需要O(N)次的交换数据即可。其他任何排序算法都不具备这个特性。
代码:
public class Solution {
public int[] MySort (int[] arr) {
int min = 0, index = 0;
for(int i=0; i<arr.length; i++){
min = arr[i];
index = i;
for(int j=i+1; j<arr.length; j++){
if(arr[j] < min){
min = arr[j];
index = j;
}
}
swap(arr, i, index);
}
return arr;
}
private void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
冒泡排序
通过相邻两个元素之间的比较和交换,使较大的元素逐渐从前面移向后面(升序),就像水底下的气泡一样逐渐向上冒泡,所以被称为“冒泡”排序。
冒泡排序的比较都是一对一对的:比较相邻元素,如果第一个比第二个大就交换,对每一个相邻的元素进行同样的工作,从开始直到最后一对,通过比较最大的数据会跑到本次的最后位置,如此反复直至整个数组有序。
时间复杂度: O(N^2)
空间复杂度 :O(1)
代码:
public class Solution {
public int[] MySort (int[] arr) {
if(arr == null || arr.length < 2) return arr;
for(int i=arr.length-1; i>=0; i--){
for(int j=0; j<i; j++){
if(arr[j] > arr[j+1]){
swap(arr,j,j+1);
}
}
}
return arr;
}
private void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
冒泡排序和选择排序,都是每轮确定一个最值。只不过选择排序是直接确定最值,而冒泡排序是通过交换相邻两个元素将最值一下一下的传递,就像水底下的气泡一样逐渐向上冒泡。二者的时间负责度和空间复杂度相同。
插入排序
插入排序是将一个数插入到已经有序的数组中,类似人们整理桥牌的方法是一张一张的来,将每一张牌插入到其他已经有序的牌的适当位置。一个一个数的插入,当数组最后一个数字插入表示整个数组已经有序。
时间复杂度 O(N^2)
空间复杂度 O(1)
代码:
public class Solution {
public int[] MySort (int[] arr) {
if(arr == null || arr.length < 2) return arr;
for(int i=1; i<arr.length; i++){
for(int j=i; j>0 && arr[j] < arr[j-1]; j--){
swap(arr, j, j-1);
}
}
return arr;
}
private void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
与选择排序不用,插入排序所需的时间取决于输入中元素的初始顺序,因为插入排序内循环每次所需要的时间和输入元素直接相关。插入排序对于部分有序的数组十分高效,也很适合小规模的数组。
折半插入排序:折半插入排序是直接插入排序与折半查找二者的结合,仍然是将待排序元素插入到前面的有序序列,插入方式也是由后往前插,只不过直接插入排序是边比较边移位。而折半插入排序则是先通过折半查找找到位置后再一并移位,最终将待排序元素赋值到空出位置。
时间复杂度 O(N^2)
空间复杂度 O(1)
折半插入排序只是优化了插入位置的方法,减少了比较的次数。而且折半插入排序的比较次数与插入排序的比较次数不同,与输入元素顺序无关,比较次数并不会随着输入不同而改变,但是元素移动的次数和插入排序相同,。
代码:
public class Solution {
public int[] MySort (int[] arr) {
if(arr == null || arr.length < 2) return arr;
for(int i=1; i<arr.length; i++){
int temp = arr[i];
int left = 0, right = i-1;
while(left <= right){
int mid = left + (right - left) / 2;
if(temp > arr[mid]){
left = mid + 1;
}else{
right = mid - 1;
}
}
for(int j=i-1; j>=left; j--){
arr[j+1] = arr[j];
}
arr[left] = temp;
}
return arr;
}
private void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
希尔排序
希尔排序是以插入排序为基础的。基本思想是先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序(即只分为一个子序列)。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率很高。
时间复杂度:由于希尔排序的时间复杂度依赖增量序列函数,所以时间复杂度数学上尚未确定。不过平均为O(N^1.3),最坏的情况下O(N ^2)
空间复杂度:O(1)
代码:
public class Solution {
public int[] MySort (int[] arr) {
if(arr == null || arr.length < 2) return arr;
int len = arr.length;
int h = 1;
while(h < len/3) h = h*3 + 1;
while(h >= 1){
for(int i=h; i<len; i++){
for(int j=i; j>=h && arr[j] < arr[j-h]; j-=h){
swap(arr, j, j-h);
}
}
h = h / 3;
}
for(int j=i-1; j>=left; j--){
arr[j+1] = arr[j];
}
arr[left] = temp;
}
return arr;
}
private void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
归并排序
归并排序的核心思想是先让序列左半部分、右半部分有序,然后合并左右两个子序列,使整个序列有序。
时间复杂度: O(NlogN)
空间复杂度: O(N)
代码:
自底向上归并
public class Solution {
private static int[] aux;
public int[] MySort (int[] arr) {
// write code here
if(arr == null || arr.length < 2) return arr;
int len = arr.length;
aux = new int[len];
for(int sz = 1; sz <len; sz = sz + sz){
for(int left=0; left<len-sz; left += sz +sz){
merge(arr, left, left + sz-1, Math.min(left + sz +sz -1, len -1));
}
}
return arr;
}
private void merge(int[] arr, int left, int mid, int right){
int i=left, j=mid+1;
for(int k=left; k<=right; k++){
aux[k] = arr[k];
}
for(int k=left; k<=right; k++){
if(i > mid) arr[k] = aux[j++];
else if(j > right) arr[k] = aux[i++];
else if(aux[i] < aux[j]) arr[k] = aux[i++];
else arr[k] = aux[j++];
}
}
}
自顶向下归并(递归)
public class Solution {
private static int[] aux;
public int[] MySort (int[] arr) {
// write code here
if(arr == null || arr.length < 2) return arr;
int len = arr.length;
aux = new int[len];
sort(arr, 0, len-1);
return arr;
}
private void merge(int[] arr, int left, int mid, int right){
int i=left, j=mid+1;
for(int k=left; k<=right; k++){
aux[k] = arr[k];
}
for(int k=left; k<=right; k++){
if(i > mid) arr[k] = aux[j++];
else if(j > right) arr[k] = aux[i++];
else if(aux[i] < aux[j]) arr[k] = aux[i++];
else arr[k] = aux[j++];
}
}
private void sort(int[] arr, int left, int right){
if(right <= left) return;
int mid = left + (right - left) / 2;
sort(arr, left, mid);
sort(arr, mid+1, right);
merge(arr, left, mid, right);
}
}
递归实现的归并排序使分治思想的典型应用。
快速排序
快速排序是一种分治的排序算法,它将一个数组分成两个子数组,然后将两部分独立的排序。
快速排序(Quick Sort) 是对冒泡排序的一种改进方法,在冒泡排序中,进行元素的比较和交换是在相邻元素之间进行的,元素每次交换只能移动一个位置,所以比较次数和移动次数较多,效率相对较低。而在快速排序中,元素的比较和交换是从两端向中间进行的,较大的元素一轮就能够交换到后面的位置,而较小的元素一轮就能交换到前面的位置,元素每次移动的距离较远,所以比较次数和移动次数较少,速度较快,故称为“快速排序”。
时间复杂度:O(NlogN)
空间复杂度:O(logN) 主要是递归造成的栈空间的使用
代码:
public class Solution {
public int[] MySort (int[] arr) {
if(arr == null || arr.length < 2) return arr;
int len = arr.length;
quicksort(arr, 0, len-1);
return arr;
}
private void quicksort(int[] arr, int left, int right){
if(left >= right) return;
int j = partition(arr, left, right);
quicksort(arr, left, j-1);
quicksort(arr, j+1, right);
}
private int partition(int[] arr, int left, int right){
int i = left, j = right + 1;
int v = arr[left];
while(true){
while(arr[++i] < v) if(i == right) break;
while(arr[--j] > v) if(j == left) break;
if(i >= j) break;
swap(arr, i, j);
}
swap(arr, left, j);
return j;
}
private void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,将有序的子数组归并以将整个数组排序,而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。在归并排序中,递归调用发生在处理整个数组之前,在快速排序中递归调用发生在处理整个数组之后。
优先队列
优先队列适用于需要处理有序的元素,但又不需要他们全部有序,或者不需要一次性就将他们排序。比如最大的K个数。优先队列数据结构应该支持两种操作:删除最大的元素(或者删除最小的元素)、插入元素。优先队列可以基于二叉堆实现,也叫做堆排序,同时优先队列还可以基于数组实现。
时间复杂度:O(NlogN)
空间复杂度:O(1)
二叉堆实现:
每个节点的值都大于其左孩子和右孩子的,叫大顶堆。
每个节点的值都小于其左孩子和右孩子的,叫小顶堆。
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if(k == 0) return new int[0];
PriorityQueue<Integer> queue = new PriorityQueue(k, new Comparator<Integer>(){
public int compare(Integer p1, Integer p2){
return p2- p1;
}
});
for(int i=0; i<k; i++){
queue.offer(arr[i]);
}
for(int i=k; i<arr.length; i++){
if(arr[i] < queue.peek()){
queue.poll();
queue.offer(arr[i]);
}
}
int[] res = new int[k];
for(int i=0; i<k; i++){
res[i] = queue.poll();
}
return res;
}
}
桶排序
桶排序的基本思想是将一个数据表分割成许多buckets,然后每个bucket各自排序,或用不同的排序算法,最后各个桶合并成为排序后的数组。
时间复杂度:O(N)
空间复杂度:O(N)
public int[] MySort (int[] arr) {
// write code here
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
// 计算桶的数量
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketArr.add(new ArrayList<Integer>());
}
// 将每个元素放入桶
for(int i = 0; i < arr.length; i++){
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
// 对每个桶进行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
// 将桶中的元素赋值到原序列
int index = 0;
for(int i = 0; i < bucketArr.size(); i++){
for(int j = 0; j < bucketArr.get(i).size(); j++){
arr[index++] = bucketArr.get(i).get(j);
}
}
return arr;
}
桶排序有两个体现:
计数排序:如以词汇出现频率排序的词频排序
基数排序:改进了计数排序,以数的区域划分桶的接收范围
计数排序就是一个数值就是一个桶,最后根据出现频率形成有序数组。而基数排序是根据元素的特征来划分通,在比较数字时,基数排序就是根据单位数组0~9来划分桶,进行最大值位数轮排序后形成有序数组。桶排序要求数据偏离程度不要太大,否则排序效果不好。
基数排序
基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或binsort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
时间复杂度:O(d(n+r))
空间复杂度:O(r)r为所采取的基数
对于数字来说,r一般为10及数字0~9
public int[] MySort (int[] arr) {
int max = arr[0];
for(int num: arr){
max = Math.max(max, num);
}
for(int exp = 1; max/exp > 0; exp*=10){
int[] temp = new int[arr.length];
int[] buckets = new int[10];
for(int value : arr){
buckets[(value / exp) % 10]++;
}
for(int i=1; i<10; i++){
buckets[i] += buckets[i-1];
}
for (int i = arr.length - 1; i >= 0; i--) {
temp[buckets[(arr[i] / exp) % 10] - 1] = arr[i];
buckets[(arr[i] / exp) % 10]--;
}
System.arraycopy(temp, 0, arr, 0, arr.length);
}
return arr;
}
计数排序
根据获得的数据表的范围,分割成不同的buckets,然后直接统计数据在buckets上的频次,然后顺序遍历buckets就可以得到已经排好序的数据表。
计数排序并不是比较排序,是先确定数据出现的范围,然后统计数据出现的次数,最后根据数据的范围由小到大的形成排序好的数组。因为范围不定,所以空间复杂度未知。
时间复杂度:O(N)
桶排序可以将排序算法的时间复杂度降低到 O(N) ,但是有两个前提需要满足: 一是需要排序的元素必须是整数,二是排序元素的取值要在一定范围内,并且比较集中 。只有这两个条件都满足,才能最大程度发挥计数排序的优势。