学习内容
1. 排序基本概念
排序(soring)是按照关键字来进行非递减或者非递增的方式对一组数据重新排列的操作。
排序的稳定性:概念过于复杂,此处我举个例子来帮助理解,如果组数据,如“2、3、4、3、5” ,这一组数据进行排序,在进行完排序之后3仍然在 3 之前那么可以说这个排序是稳定的,反之则不稳定。
(1) 内部排序和外部排序
因为待排序想数据的数量不同,存储数据的设备也会有所不同,总不可能在内存里面存储超过他存储容量的数据。
内部排序:待排序记录全部放在计算机内存中进行排序的过程。
外部排序:待排序数据很多,内存一次不能容纳全部数据,在排序过程中需要对外存进行访问的排序。
2. 各个排序内容
(1)插入排序
a、直接插入排序
将一个数据插入到已经排好序的有序表中,从而得到一个新的记录加一的有序表
//插入排序
public static void insertSort(int[] arr) {
int bound = 1;
//升序排列
//[0,bound)是已经排序区间
//[bound,length)是待排序区间
for (; bound < arr.length; bound++) {
//执行比较插入过程
int v = arr[bound];
int cur = bound - 1;
//节省了交换操作,直接记录之后找到位置直接插入
for (; cur >= 0; cur--) {
//如果当前这个数大于待排序区间第一个数
//注意:这块不能加入等于,因为插入排序是稳定排序
if (arr[cur] > v) {
//就将它往后面赋值
arr[cur + 1] = arr[cur];
}else {
//不大于则停止
break;
}
}
//将待排序区间的第一个数放入相应位置
//你已经将大于他的数全部后移了
arr[cur + 1] = v;
}
}
时间复杂度:O(n2)
空间复杂度:O(1)
稳定排序
算法特点:更适合于初始记录基本有序(正序)的情况。 不适用于初始数据无序,bound比较大的情况,这样会导致算法时间复杂度高,不宜采用。
b、希尔排序
希尔排序又叫缩小增量排序,也是插入排序的一种。
上面说到,直接插入排序的缺点,希尔排序就是针对直接插入排序的缺点来进行设计。
从减少数据个数,以及数列基本有序的方面来对插入排序进行了改进
希尔排序就是将数据分为多个组,对每一组进行直接插入排序
//希尔排序
public static void shellSort(int[] arr) {
//求出每一躺的增量
//由大往小
int gap = arr.length / 2;
while (gap >= 1) {
//进行分组,并对每一组直接插入排序
_shellSort(arr,gap);
gap = gap / 2;
}
}
public static void _shellSort(int[] arr,int gap) {
//从第二部分开始
//和第一部分比较来开始希尔排序
//不是每一组整体比较,而是混合比较
//每隔几个分为一组
int bound = gap;
for (;bound < arr.length; bound++) {
//还是直接插入排序的思想、
//不过是每一组之间进行排序
//还有就是每一组的各个元素轮番进行比较
int v = arr[bound];
int cur = bound - gap;
for (; cur >= 0; cur -= gap) {
if (arr[cur] > v) {
arr[cur + gap] = arr[cur];
}else{
break;
}
}
arr[cur + gap] = v;
}
}
时间复杂度:平均情况O(n1.3)
空间复杂度:O ( 1 )
不稳定排序
算法特点只能用于顺序结构,不能用于链式结构,对于增量的取值也应该是,除了一之外没有其他的公因子,并且最后一个增量值必须是1。比较适合初始数据无序,n比较大的情况。
(2)交换排序
交换排序就是来那个量比较待排序数据的关键字,一旦两个数据不满足次序要求是则进行交换,知道整个序列全部满足要求为止。
a.冒泡排序
两两比较相邻记录的关键字,如果发生逆序,进行交换,从而使关键字小的记录像气泡一样上升。
首先先实现交换的方法
public static void swap(int[] arr, int parent, int child) {
int tmp = arr[parent];
arr[parent] = arr[child];
arr[child] = tmp;
}
之后就是冒泡排序
//冒泡排序
public static void bubbleSort(int[] arr) {
//先从第一个元素开始
for (int bound = 0; bound < arr.length; bound++) {
//当前cur为第二个元素
//下次排序减掉已经排序的区间
for (int cur = 1; cur < arr.length - bound; cur++) {
//让他与第一个元素比较,因为是圣墟排列,当前一个大于后一个就交换
if (arr[cur - 1] > arr[cur]) {
swap(arr,cur - 1,cur);
}
}
}
}
时间复杂度 :O(n2)。
空间复杂度 :O(1)。
排序稳定
算法特点:可以用于链式结构,移动记录次数越多,算法平均时间性能比直接插入排序差。
b.快速排序
从一组数据中取出一个作为支点或者枢轴,把大于这个数的数据放后面,小于这个数的数据放前面,之后将支点放在分界处的位置,然后分别对左右子表重复上面操作,知道每一个子表只有一个元素,排序完成。
//快速排序
public static void quiteSort(int[] arr) {
//辅助递归
_quiteSort(arr, 0, arr.length-1);
}
public static void _quiteSort(int[] arr, int left, int right) {
//左右下标重合,退出递归
if (left >= right) {
return;
}
//对一组进行快速排序得到中间结点的下标
int index = partition(arr,left,right);
//对子表进行快速排序
_quiteSort(arr,left,index-1);
_quiteSort(arr,index+1,right);
}
public static int partition(int[] arr, int left, int right) {
int v = arr[right];
int l = left;
int r = right;
//直到左右遍历完,结束
while (l < r) {
//循环找到 从支点左侧循环找出第一个大于支点的数
while (l < r && arr[l] <= v) {
l++;
}
//从支点右侧找到第一个小于支点的数
while (l < r && arr[r] >= v) {
r--;
}
//交换两个数
swap(arr,l,r);
}
//此处的 right 不是 r 而是作为支点的数
//每次都用最右侧的数作为支点
//完成左右比较之后,将支点放在中间位置
//虽然进行了比较但是支点的位置还是没变的依旧在最右侧
swap(arr,l,right);
//此时 l 已经到达数组的相对的中间的地方,返回这一下标
return l;
}
public static void swap(int[] arr, int parent, int child) {
int tmp = arr[parent];
arr[parent] = arr[child];
arr[child] = tmp;
}
时间复杂度 :O(nlog2n)
空间复杂度:快速排序是递归的,需要有一个栈来村方向应的数据。最好情况O(log2n)最坏情况O(n)
排序不稳定
算法特点:适合排序无序,数据量大的情况
(3)选择排序
a.简单选择排序
(升序)每次都找到最小的放到未排序的序列的第一个
//选择排序
public static void selectSort(int[] arr) {
//[0,bound)已排序区间
for (int bound = 0; bound < arr.length; bound++) {
for (int cur = bound + 1; cur < arr.length; cur++) {
//找到 最小的数 放到 第一个
if (arr[bound] > arr[cur]) {
swap(arr,bound,cur);
}
}
}
}
public static void swap(int[] arr, int parent, int child) {
int tmp = arr[parent];
arr[parent] = arr[child];
arr[child] = tmp;
}
时间复杂度 :O(n2)
空间复杂度 :O(1)
稳定排序 此处也可能由于交换策略 会造成不稳定的现象
b.堆排序
利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序的序列中选择关键字(数)最大的(或最小的)数据。
public static void heapSort(int[] arr) {
creatHeap(arr);
//循环去除堆顶元素和最后一个元素交换并删除
int heapSize = arr.length;
for (int i = 0; i < arr.length; i++) {
//交换0号元素和堆的最后一个元素
swap(arr,0,heapSize - 1);
//删除
heapSize--;
//从0号位置往下进行调整
shiftDown(arr,heapSize,0);
}
}
public static void creatHeap(int[] arr) {
//建大根堆
//向下调整需从后往前,要将大的数,通过循环一步一步往上挪动
for (int i = arr.length/ 2; i >= 0; i--) {
shiftDown(arr,arr.length,i);
}
}
public static void shiftDown(int[] arr, int size, int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < size) {
//找子树较大的一个
if (child + 1 < size && arr[child + 1] > arr[child] ) {
child = child + 1;
}
//比较较大的子树是否比父节点大
if (arr[parent] < arr[child]) {
swap(arr,parent,child);
}else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
时间复杂度 :O(nlog2n)
空间复杂度 :O(1)
排序不稳定
算法特点 :只能用于顺序结构,比较次数较多,当数据较少的时候不建议使用。
(4)归并排序
将两个或者两个以上的有序表合并成一个有序表的过程
//归并排序
public static void mergeSort(int[] arr) {
_mergeSort(arr,0,arr.length);
}
public static void _mergeSort(int[] arr, int left, int right) {
//当前有序表只有一个元素
if (right - left <= 1) {
return;
}
int mid = (left + right) / 2;
//[left,mid)
//[mid,right)
_mergeSort(arr,left,mid);
_mergeSort(arr,mid,right);
merge(arr,left,mid,right);
}
//合并两个有序数组数组为一个有序数组
//待合并的两个数组就分别是
//[left,mid)
//[mid,right)
public static void merge(int[] arr,int left,int mid,int right) {
//创建一个临时数组,用来存放合并结果
//为了这个数组能存下合并后结果 right - left
int[] tmp = new int[right - left];
//把新的元素放在tmp哪个下标上
int tmpSize = 0;
int l = left;
int r = mid;
while (l < mid && r < right) {
if (arr[l] <= arr[r]) {
tmp[tmpSize] = arr[l];
tmpSize++;
l++;
} else {
tmp[tmpSize] = arr[r];
tmpSize++;
r++;
}
}
//当其中一个数组遍历完之后,就把另外一个数组剩余的部分拷贝过来
while (l < mid) {
tmp[tmpSize] = arr[l];
tmpSize++;
l++;
}
while (r < right) {
tmp[tmpSize] = arr[r];
tmpSize++;
r++;
}
//把tmp中的内容拷贝会[left,right) 中 。
for (int i = 0; i < tmp.length; i++) {
arr[left + i] = tmp[i];
}
}
时间复杂度 :O(nlog2n)
空间复杂度 :O(n)
排序稳定
算法特点:可以使用链式结构,不需要附加存储空间,但是在使用递归时仍然需要递归的工作栈