排序: 使一组数据,按照其中某个或者某些关键字的大小,递增或者递减的排列起来的操作
稳定性:在一组存在重复数据的记录中,经过排序算法后,这些相等数据的相对次序没有改变,则称这种算法是稳定的.
内部排序:数据记录在内存中进行排序
外部排序:因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存
基本思想:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
算法步骤:
将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
public static void insertSort1(int[] array) {
for (int i = 1; i < array.length; i++) {
//记录插入的数据
int tmp = array[i];
int j = i - 1;
//j位置前的序列已经有序,所以这里从最后开始比较
for (; j >= 0; j--) {
//如果这里变成大于等于就变成不稳定的
if (array[j] > tmp) {
array[j + 1] = array[j];
} else {
break;
}
}
array[j + 1] = tmp;
}
}
//code - 2
public static void insertSort2(int[] array) {
int n = array.length;
for (int i = 0; i < n; i++) {
//寻找元素arr[i]合适位置插入
for (int j = i; j > 0; j--) {
if (array[j] < array[j-1]) {
swap(array, j, j - 1);
} else {
break;
}
}
}
}
private static void swap(int[] array, int a, int b) {
int tmp = array[a];
array[a] = array[b];
array[b] = tmp;
}
总结:
基本思想:
选定一个整数gap,代表数据之间的间隔, 距离相同的数据在同一组,并对每一组内的数据排序,然后把gap缩小,重复比较,直到gap==1时,数据已经有序
算法步骤:
public static void shell(int[] arr, int gap){
for (int i = gap; i < arr.length; i++) {
int tmp = arr[i];
//与插入排序相似,插入排序的gap等于1,而希尔排序的gap在变化,最终gap会等于1
int j = i - gap;
for (; j >= 0; j--) {
if (arr[j] > tmp) {
arr[j + gap] = arr[j];
} else {
break;
}
}
arr[j + gap] = tmp;
}
}
public static void shellSort1(int[] array) {
int gap = array.length;
//这几次都是预排序,使数组变得比较有序
while (gap > 1) {
shell(array, gap);
gap = gap >>> 2;
}
//真正的使数组有序
shell(array, 1);
}
总结:
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
基本步骤:
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i + 1; j < array.length; j++) {
//minIndex始终保存最小值索引,在比较时其下标会更新.
if (array[j] < array[minIndex]) {
minIndex = i;
}
}
swap(array, minIndex, i);
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
总结:
基本思想:
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点 ,分为两种方法.排升序要建大堆,排降序建小堆。,如果不懂堆可以看看这篇文章链接
基本步骤:
原因:因为我们创建的是大根堆,所以在调整时,最大的元素始终会在堆顶,然后我们交换堆顶和堆尾元素,每一次都使堆中的最大元素到堆尾去,然后减少堆大小,从而最终数据有序化
public static void heapSort(int[] arr) {
//建堆
createHeap(arr);
int end = arr.length - 1;
while (end >= 0) {
swap(arr, 0, end);
shiftDown(arr, 0, end);
end--;
}
}
/**
* 向下调整 -
* 时间复杂度 0(logN)
* @param arr 二叉树中存放的元素
* @param root 根节点的下标
* @param len 数组长度
*/
private static void shiftDown(int[] arr, int root, int len) {
int parent = root;
int child = (2 * parent) + 1;//左孩子
//孩子节点索引不会大于len
while (child < len) {
//使child索引位置保存子节点的最大值
if (child + 1< len && arr[child] < arr[child + 1]) {
child++;
}
if (arr[child] > arr[parent]) {
swap(arr, child, parent);
parent = child;
child = (2 * parent) + 1;
} else {
break;
}
}
}
/**
* 建堆
* 时间复杂度 0(n)
* @param arr
*/
private static void createHeap(int[] arr) {
for (int p = (arr.length - 1 - 1) / 2; p >=0; p--) {
shiftDown(arr, p, arr.length);
}
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
总结:
基本思想:
根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置
基本步骤:
它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来.
public static void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
boolean flag = false;
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
flag = true;
}
}
if (!flag) {
break;
}
}
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
总结:
快排是由Tony Hoare于1962年提出的一种二叉树结构的交换排序方法.
基本思想:
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
算法步骤:
基本框架:
public static void quickSort(int[] arr) {
quick(arr, 0, arr.length - 1);
}
private static void quick(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int pivot = partitionHoare(arr, left, right);
//递归排(left, pivot - 1]
quick(arr, left, pivot - 1);
//递归排排(pivot + 1, right]
quick(arr,pivot + 1, right);
}
通过观察可以看出其基本框架和二叉树的前序遍历十分相似,大家可以在写递归框架时联想到前序遍历.下面就是分区的方法:
基本步骤:
/**
* Hoare法
* 如果是顺序或者逆序,递归退化为单分枝, 为 o(N^2) 平均为 o(N*logN)
* @param arr
* @param low
* @param high
* @return
*/
private static int partitionHoare(int[] arr, int low, int high) {
int i = low;
int tmp = arr[low];
while (low < high) {
//左边找比基准大的
while (low < high && arr[high] >= tmp) {
high--;
}
//右边找比基准小的
while (low < high && arr[low] <= tmp) {
low++;
}
swap(arr, low, high);
}
//把基准挪到它真正的位置
swap(arr,low,i);
//返回新的基准
return low;
}
三数取中法取key
我们先思考一个问题,如果数据是有序,那么二叉树退化为单分枝,时间复杂度为0(N*2),为了减少比较,我们可以随机取key,但是它比较靠运气,所以就出现了三数取中法,它主要是为了解决排序接近有序或者有序的情况下快速排序的效率将会非常的低,并且递归的深度非常的深导致的问题。
private static int middleOfThreeIndex(int[] arr, int left, int right) {
//注意这里符号的优先级,加括号的位置
int mid = left + ((right - left) >>> 1);
if (arr[left] < arr[right]) {
if (arr[mid] < arr[left]) {
return left;
} else if (arr[mid] > arr[right]) {
return right;
} else {
return mid;
}
} else {
if (arr[mid] < arr[right]) {
return right;
} else if (arr[mid] > arr[left]) {
return left;
}else {
return mid;
}
}
}
递归到小的子区间时,考虑使用插入排序
private static void quick(int[] arr, int left, int right) {
if (left >= right) {
return;
}
//1.数据越来越接近有序,所以用直接插入排序,速度快,这里的区间根据数据量来设置
if (right - left + 1 <= 7000) {
insertSort(arr, left, right);
return;
}
//2.三数取中法 减少递归深度,避免单分枝
int index = middleOfThreeIndex(arr, left, right);
swap(arr,left, index);
int pivot = partitionHoare(arr, left, right);
//递归排(left, pivot - 1]
quick(arr, left, pivot - 1);
//递归排排(pivot + 1, right]
quick(arr,pivot + 1, right);
}
基本步骤:
常见问题及解释:
这是为了确保最后相遇时array[low] < array[key], 只要让右边的数先走,最后停下来时无论是“左边遇到右边”还是“右边遇到左边”,都满足array[low]
解决重复元素问题,因为在遍历序列中我们找的仅仅是比key大或小的元素,如果等于的话就直接跳过。
private static int partitionHole(int[] arr, int low, int high) {
//把key挖出
int tmp = arr[low];
while (low < high) {
//右边找比key小的
while (low < high && arr[high] >= tmp) {
high--;
}
//小的放到左坑中,自己形成新的坑
arr[low] = arr[high];
//左边找比key大的
while (low < high && arr[low] <= tmp) {
low++;
}
//现在左坑是小值的,然后把大的放到右边
arr[high] = arr[low];
}
//最后,把key放到它真正的位置
arr[low] = tmp;
return low;
}
基本步骤:
private static int partitionPointer2(int[] array, int low, int high) {
int key = low;
int prev = low ;
int cur = low+1;
while (cur <= high) {
if(array[cur] < array[key] && ++prev != cur) {
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,key);
return prev;
}
总结:
递归版本会在递归过程中会占用巨大的栈空间,导致栈溢出,所以我们使用栈来模拟实现递归过程
void QuickSortNonR(int[] a, int left, int right)
{
Stack<Integer> st = new Stack<>();
st.push(left);
st.push(right);
while (!st.empty())
{
right = st.pop();
left = st.pop();
if(right - left <= 1)
continue;
int div = partitionHoare(a, left, right);
// 以基准值为分割点,形成左右两部分:[left, div) 和 [div+1, right)
st.push(div+1);
st.push(right);
st.push(left);
st.push(div);
}
}
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
private static void merge(int[] array,int low,int mid,int high) {
int s1 = low;
int e1 = mid;
int s2 = mid+1;
int e2 = high;
int[] tmpArr = new int[high-low+1];
int k = 0;//代表tmpArr的下标
//证明两个段都有数据
while (s1 <= e1 && s2 <= e2) {
if(array[s1] <= array[s2]) {
tmpArr[k++] = array[s1++];
}else {
tmpArr[k++] = array[s2++];
}
}
//剩余元素,全部挪过来
while (s1 <= e1) {
tmpArr[k++] = array[s1++];
}
//剩余元素,全部挪过来
while (s2 <= e2) {
tmpArr[k++] = array[s2++];
}
for (int i = 0; i < tmpArr.length; i++) {
array[i+low] = tmpArr[i];
}
}
private static void mergeSortInternal(int[] array,int low,int high) {
if(low >= high) return;
int mid = low+((high-low)>>>1);
mergeSortInternal(array,low,mid);
mergeSortInternal(array,mid+1,high);
merge(array,low,mid,high);
}
public static void mergeSort(int[] array) {
mergeSortInternal(array,0,array.length-1);
}
总结:
public static void mergeSortNor(int[] array) {
int gap = 1;
while (gap < array.length) {
for (int i = 0; i < array.length; i += 2*gap) {
int left = i;
int mid = left+gap-1;
//修正mid
if(mid >= array.length) {
mid = array.length-1;
}
int right = mid+gap;
//修正right
if(right >= array.length) {
right = array.length-1;
}
merge(array,left,mid,right);
}
gap *= 2;
}
}
基本思想:
将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
基本步骤:
public static void countSort(int[] array) {
//1、获取最大值和最小值
int maxVal = array[0];
int minVal = array[0];
//O(N)
for (int i = 1; i < array.length; i++) {
if(array[i] < minVal) {
minVal = array[i];
}
if(array[i] > maxVal) {
maxVal = array[i];
}
}
//2、开始计数
int range = maxVal-minVal+1;
int[] count = new int[range];
//O(N)
for (int i = 0; i < array.length; i++) {
count[array[i]-minVal]++;
}
//3、遍历这个计数数组,把数据放回array
//O(范围 + n), 某一次 可能是n次 有可能一个数重复重新多次
int index = 0;
for (int i = 0; i < count.length; i++) {
while (count[i] > 0) {
array[index++] = i+minVal;
count[i]--;
}
}
}
总结:
基本思想:
利用了函数的映射关系,把具有这种关系(在一定范围的元素)的元素放到一个桶中,进行排序,高效与否的关键就在于这个映射函数的确定.
public static void bucketSort(int[] arr) {
// 1. 计算最大值和最小值
int max = arr[0];
int min = arr[0];
for (int x : arr) {
max = Math.max(max, x);
min = Math.min(min, x);
}
//2.计算桶的数量
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<>());
}
//3.将每一个元素入桶
for (int i = 0; i < arr.length; i++) {
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
//4.对每一个桶排序
for (int i = 0; i < bucketArr.size(); i++) {
Collections.sort(bucketArr.get(i));
}
//把桶中元素重新赋值
int pos = 0;
for (int i = 0; i < bucketArr.size(); i++) {
for (int j = 0; j < bucketArr.get(i).size(); j++) {
arr[pos++] = bucketArr.get(i).get(j);
}
}
}
总结:
基本思想:
将整数按位数切割成不同的数字,然后按每个位数分别比较
public static void radixSort(int[] arr) {
//1.获取基数 - 也就是最大位数 - 决定比较次数
int radix = getRadix(arr);
//2.创建桶 list所有桶的集合 每一个桶是LinkedList当成队列来用,
//创建0~9个桶(桶的底层是队列),因为所有的数字元素都是由0~9的十个数字组成
LinkedList<Integer>[] lists = new LinkedList[10];
//初始化
for (int i = 0; i < lists.length; i++) {
lists[i] = new LinkedList<>();
}
//3.开始 分类-装桶
//依次判断每个元素的个位,十位至MAX位,存入对应的桶中,出队,存入原数组;直至MAX轮结束输出数组。
for (int r = 1; r <= radix; r++) {
//分类过程
for (int i = 0; i < arr.length; i++) {
lists[getIndex(arr[i], r)].offer(arr[i]);
}
int index = 0;
//放到原数组
for (int i = 0; i < lists.length; i++) {
while (!lists[i].isEmpty()) {
arr[index++] = lists[i].poll();
}
}
}
}
/**
* 获取下标
* @param num
* @param r
* @return
*/
private static int getIndex(int num, int r) {
int ret = 0;
for (int i = 1; i <= r; i++) {
ret = num % 10;
num /= 10;
}
return ret;
}
/**
* 获取最大位数
* @param arr
* @return
*/
private static int getRadix(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return (max + "").length();
}
排序算法复杂度及稳定性分析