目录
1.排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
2.常见的排序算法
2.1插入排序
把待排序的记录按照其关键值的大小逐一插到一个已经排好序的有序序列当中,直到所有的记录插完为止,得到一个新的有序序列。
当插入第i个元素时,前面array[0]~array[i - 1]个元素已经有序,此时用array[i]的排序码与array[i - 1],array[i - 2]......的排序码进行比较,找到符合条件位置插入即可。
public static void insertSort(int[] array){
//处理循环层
for(int i = 1; i < array.length; i++){
//把i下标的值保存起来
int temp = array[i];
//内层循环
int j = i - 1;
//一直遍历到数组开头
while(j >= 0){
//证明j之前的数都有序
if(array[j] <= temp){
break;
}
// 移动j的值到j+1
array[j + 1] = array[j];
j--;
}
// 把temp的值放到j+1位置
array[j + 1] = temp;
}
}
插入排序特性总结:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
如果元素趋于有序,此算法的效率越高
2.2希尔排序 (缩小增量排序)
希尔排序也叫缩小增量排序:先确定一个整数gap,把待排序元素分成若干个组,所有相距大小为gap的为一组,并对每一组内元素进行排序,重复上述的工作,当gap = 1时,所有的元素已经有序
public static void shellSort (int[] array) {
// 1. 确定增量的大小
int gap = array.length / 2;
while (gap > 0) {
shell(array, gap);
// gap每次变成原来的1/2
gap /= 2;
}
}
private static void shell(int[] array, int gap) {
// 1. i 从gap位置开始向后遍历
for (int i = gap; i < array.length; i++) {
// 2. i下标的值记录下来
int temp = array[i];
// 3. 定义j 下标
int j = i - gap;
// 4. 开始循环
while (j >= 0) {
// 如果j下标的值比temp小,说明前面已经有序了
if (array[j] <= temp) {
break;
}
// 称动j下标的值到j+gap下标
array[j + gap] = array[j];
// j向数组头部移动gap
j -= gap;
}
// 把temp放在j+gap下标位置
array[j + gap] = temp;
}
}
希尔排序特性总结:
时间复杂度:O(N^1.25) ~ O(N^1.5)
空间复杂度:O(1)
稳定性:不稳定
希尔排序是直接插入排序的优化
2.3选择排序
选择排序的基本思想:每一次从待排序元素中选择出最小(或者最大)的一个元素,存放在序列的起始位置,直到全部待排序元素排完。
首先记录最小值下标minIndex,默认是下标是i
从i + 1位置遍历到数组末尾,如果有比array[minIndex]值小的,minIndex更新
遍历完成之后交换i与minIndex的值
重复上述步骤直至数组遍历完成
- 只记录最小值,然后交换排序
public static void selectSort (int[] array) {
// 1. 从0 下标开始向后遍历
for (int i = 0; i < array.length; i++) {
// 2. 记录最小值下标,默认最小值下标是i
int minIndex = i;
// 3. j从i + 1 下标开始向后遍历
for (int j = i + 1; j < array.length; j++) {
// 4. 如果找到比当前minIndex 还小的值,那么更新minIndex下标到j
if (array[j] < array[minIndex]) {
minIndex = j;
}
}
// 交换minIndex和i的元素
swap(array, minIndex, i);
}
}
private static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
- 一次选出最大值与最小值
public static void selectSort2 (int[] array) {
// 1. 定义数组边界
int left = 0;
int right = array.length - 1;
// 进入循环
while (left < right) {
// 2. 定义最小值与最大值下标
int minIndex = left;
int maxIndex = left;
for (int i = left + 1; i <= right; i++) {
// 3. 如果i 下标的值比最小值下标的值还小,那么更新最小值下标
if (array[i] < array[minIndex]) {
minIndex = i;
}
// 4. 如果i 下标的值比最大值下标的值还大,那么更新最大值下标
if (array[i] > array[maxIndex]) {
maxIndex = i;
}
}
// 5. 每次遍历完成后,left与minIndex交换
if (left != minIndex) {
swap(array, left, minIndex);
}
// 6. 修正最大值与最小值重复交换的问题
if (left == maxIndex) {
maxIndex = minIndex;
}
// 7. right与maxIndex 交换
if (right != maxIndex) {
swap(array, right, maxIndex);
}
// 8. 移动left与right
left++;
right--;
}
}
选择排序特性总结:
时间复杂度:O(N^2),与数组是否有序无关
空间复杂度:O(1)
稳定性:不稳定
2.4堆排序
堆排序是基于堆这种数据结构来进行排序的,它是选择排序的一种
注意:排升序建大堆,建小堆
主要思想:首先对待排序元素根据相应的要求建堆,然后根结点与最后一个叶子结点进行交换,调整除最后一个叶子结点外的其他结点成为一个堆结构,重复上述步骤,直至元素排序完成。
建堆及其相关操作见文章------> 数据结构——堆与PriorityQueue
public static void heapSort (int[] array) {
// 1. 建堆
createHeap (array);
// 2. 确定最后一个元素的下标
int end = array.length - 1;
// 循环处理
while (end >= 0) {
// 3. 数据首尾交接
swap(array, 0, end);
// 4. 从堆顶开始向下调整
shiftDown(array, 0, end);
// 5. 修正一下end的值
end--;
}
}
// 创建堆
private static void createHeap(int[] array) {
// 找到最后一个度不为0的子树根节点
for (int parent = (array.length - 2) / 2; parent >= 0 ; parent--) {
// 向下调整
shiftDown(array, parent, array.length);
}
}
// 向下调整
private static void shiftDown(int[] array, int parent, int length) {
// 1. 根据父节点找到左孩子节点
int child = 2 * parent + 1;
// 2. 循环处理,判断是否越界
while (child < length) {
if (child + 1 < length) {
// 3. 判断左孩子节点的值与右孩子节点的值哪个大就选哪个下标
if (array[child + 1] > array[child]) {
// 右孩子节点的值大,就选右孩子节点下标
child++;
}
}
// 孩子节点中最大值与父节点的值做比较
if (array[child] <= array[parent]) {
break;
}
// 交换父子下标的值
swap(array, parent, child);
// 向下移动父子节点的下标
parent = child;
child = 2 * parent + 1;
}
}
堆排序特性总结:
时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定排序:不稳定
2.5冒泡排序
冒泡排序:是交换排序的一种,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。冒泡排序是最基础的一种排序
public static void bubbleSort (int[] array) {
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
// 如果进行比较
}
}
}
}
冒泡排序的优化:确定一个flag,在进行一个遍历后,发现所有值都没有交换,说明待排序序列已经有序,所以我们后续操作不再进行。
public static void bubbleSort (int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean flag = false;
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
// 如果进行比较
flag = true;
}
}
// 没有参与比较,说明数组已经有序了
if (!flag) {
break;
}
}
}
冒泡排序特性总结:
时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定
2.6快速排序
快速排序:快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
2.6.1Hoare法
Hoare法,首先确定基准值pivot = array[left],然后right向前走找比pivot小的值,left向后走找比pivot大值,两者找到之后交换值,然后继续寻找,直到left 与rigth相遇,交换pivot 与 left 位置。
// Hoare法找基准
private static int partitionHoare(int[] array, int left, int right) {
// 1. 先以left下标的值做为默认基准值
int pivotValue = array[left];
int pivotIndex = left;
// 2. left与right没有相遇的时候,循环处理
while (left < right) {
// 3. 让right向左移动,找到比基准小的值停下来
while (left < right && array[right] >= pivotValue) {
// 移动right
right--;
}
// 4. 让left向右移动,找到比基准值大的停下来
while (left < right && array[left] <= pivotValue) {
left++;
}
// 5. left与right交换
swap(array, left, right);
}
// 6. 相遇点与默认的基准下标做交换
swap(array, left, pivotIndex);
// 7. 返回相遇点的下标,这时相遇点下标就是基准
return left;
}
2.6.2挖坑法
挖坑法,假如确定基准值pivot为第一个元素,将第一个元素位置空出,然后right向前找小于pivot的元素,若找到后移动到空的位置,随后left向后寻找大于pivot的元素,放入空位置,继续操作,直到left 与right相遇,将基准值pivot放入位置。
// 挖坑法找基准
private static int partitionHole (int[] array, int left, int right) {
// 1. 记录默认基准值,left下标的值
int pivotValue = array[left];
// 2. 在循环中处理交换
while (left < right) {
// 3. right向左移动,找到比基准值小的位置
while (left < right && array[right] >= pivotValue) {
// 移动右下标
right--;
}
// 4. 把right下标的值放到left下标
array[left] = array[right];
// 5. left向右移动,找到比基准值大的位置
while (left < right && array[left] <= pivotValue) {
// 移动left下标
left++;
}
// 6. 把left下标的值放到right下标
array[right] = array[left];
}
// 把基准值放到相遇点的下标
array[left] = pivotValue;
// 返回相遇点的下标
return left;
}
2.6.3双指针法
快慢指针,定义prev = left ,current = left +1,基准值pivot为第一个,如果current位置的值小于基准值,prev向后走一步,此时判断current 和 prev是不是同一个位置,如果不是,两者进行交换,如果是在同一位置,current向后走一步,在遍历完成之后,prev的值与基准值进行交换即可。
// 快慢指针法找基准
private static int partitionPointer (int[] array, int left, int right) {
// 1. 定义两个变量
int prev = left;
int current = left + 1;
// 循环
while (current <= right) {
// 2. current下标的值小于基准值
if (array[current] < array[left]) {
prev++;
if (array[prev] != array[current]) {
swap(array, current, prev);
}
}
// current向后移动
current++;
}
// 遍历完成后,prev的值与基准下标的值交换
swap(array, prev, left);
// 返回prev下标
return prev;
}
2.6.4三数取中解决栈溢出
在使用排序算法的时候,少量有序数据可以完成排序,但是大量有序数据会出现一个错误———栈溢出
这是因为如果待排序数组是一个有序数组之后,基于二叉树的快速排序退化成了一个单链表,导致递归深度最大达到了N层,和元素数量一样,因此导致栈溢出。
为了解决栈溢出问题,我们采用三数取中法
三数取中法:
- 以数组最左边与最右边做基准值
- 根据最左边下标与最右边下标确定中间下标,
- 以左下标,中间下标,右下标对应的值,在这三个值中找出中间大小的值
- 获取到中间下标值之后,让mid与left下标的值做一个交换,然后再按照找基准值的方法去确定基准值。
private static int middleValueIndex (int[] array, int left, int right) {
// 1. 根据left与right计算中间下标
int middle = (left + right) / 2;
// 2. 开始处理核心逻辑
if (array[left] < array[right]) {
// 左小右大
if (array[middle] > array[right]) {
// 中间值比最大的还大
return right;
} else if (array[middle] < array[left]) {
// 中间值比最小的还小
return left;
} else {
// 本身就是中间值
return middle;
}
} else {
// 左大右小
if (array[middle] < array[right]) {
// 中间值比最小的还小
return right;
} else if (array[middle] > array[left]) {
// 中间值比最大的还大
return left;
} else {
// 本身就是中间值
return middle;
}
}
}
主方法入口
public static void quickSort(int[] array) {
quickSortProcess(array, 0, array.length - 1);
}
private static void quickSortProcess(int[] array, int left, int right) {
// 1. 终止条件
if (left >= right) {
return;
}
// =============== 三数取中法优化栈溢出问题 开始==================
int middle = middleValueIndex(array, left, right);
// 与left交换后,继续后面的找基准软软
swap(array, left, middle);
// =============== 三数取中法优化栈溢出问题 完成==================
// 2. 在区段中找基准
int pivot = partitionHoare(array, left, right); // Hoare法
// int pivot = partitionHole(array, left, right); // 挖坑法
// int pivot = partitionPointer(array, left, right); // 快慢指针法
// 3. 根据基准处理左区段和右区段
quickSortProcess(array, left, pivot - 1);
quickSortProcess(array, pivot + 1, right);
}
快速排序特性总结:
时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定
2.7归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
基本思路:
- 把大数组分成若干个小数组
- 把小数组进行归并操作,在归并的过程中进行排序
- 创建一个临时数组,大小为(right - left + 1)来存储排序好的数据
- 把临时数组拍好的数据覆盖到原始数组
public static void mergeSort (int[] array) {
mergeSortProcessor(array, 0, array.length - 1);
}
private static void mergeSortProcessor(int[] array, int left, int right) {
// 1. 终止条件
if (left >= right) {
return;
}
// 2. 找中间下标
int middle = (left + right) / 2;
// 3. 开始递归,分解左右子区段
mergeSortProcessor(array, left, middle);
mergeSortProcessor(array, middle + 1, right);
// 4. 合并过程
merge(array, left, middle, right);
}
private static void merge(int[] array, int left, int middle, int right) {
// 1. 创建一个临时数组, 注意数组的容量
int[] temp = new int[right - left + 1];
// 临时数组的当前下标
int index = 0;
// 2. 确定每一个小数组的起止下标
int start1 = left;
int end1 = middle;
int start2 = middle + 1;
int end2 = right;
// 3. 归并, 在归并的过程中完成排序
while (start1 <= end1 && start2 <= end2) {
// 判断两个数组中当前元素的大小
if (array[start1] < array[start2]) {
// 把start1下标的值加到临时数组里
temp[index++] = array[start1++];
} else {
temp[index++] = array[start2++];
}
}
// 4. 把数组中的剩余元素加入到临时数组末尾
while (start1 <= end1) {
temp[index++] = array[start1++];
}
while (start2 <= end2) {
temp[index++] = array[start2++];
}
// 5. 写回原数组
for (int i = 0; i < temp.length; i++) {
array[i + left] = temp[i];
}
}
归并排序特性总结:
时间复杂度:O(N * logN)
空间复杂度:O(N)
稳定性:稳定
2.8计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中
在理想状态下我们可以简单使用计数排序,但是一些比较大的数据,例如1002,1000,1006,1012.....,这些数据较大,我们不能开辟这么大的内存空间来完成,这样会严重浪费资源,我们处理的方法如下:
- 遍历数组,找到数组的最大值max与最小值min
- 创建一个数组,容量大小为max - min + 1;
- 再次遍历原始数组,用元素减去min,得到计数数组的下标,让计数数组对应下标的值加一
- 遍历计数数组,根据对应值输出N个元素, 值 = 下标 + min
public static void counterSort(int[] array){
int minValue = array[0];
int maxValue = array[0];
for (int i = 0; i < array.length; i++) {
if(array[i] < minValue){
minValue = array[i];
}
if(array[i] > maxValue){
maxValue = array[i];
}
}
int[] countArray = new int[maxValue - minValue + 1];
//再次遍历数组记录到对应元素的个数
for (int i = 0; i < array.length; i++) {
int index = array[i] - minValue;
countArray[index]++;
}
//遍历计数数组
int index = 0;
for (int i = 0; i < countArray.length; i++) {
while (countArray[i] > 0){
int value = i + minValue;
array[index] = value;
countArray[i]--;
index++;
}
}
}
计数排序的特性总结:
计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
时间复杂度:O(MAX(N,范围))
空间复杂度:O(范围)
稳定性:稳定
2.9基数排序(简单介绍)
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
2.10桶排序 (简单介绍)
一句话总结:划分多个范围相同的区间,每个子区间自排序,最后合并。
3.排序总结