您现在的位置是:首页 >学无止境 >【Java数据结构】排序网站首页学无止境
【Java数据结构】排序
插入排序
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
实际中我们玩扑克牌时,就用了插入排序的思想。
实现
/**
* 插入排序
* 时间复杂度:
* 最好情况下:O(n) -> 数据有序的情况下 1 2 3 4 5
* 最坏情况下:O(n^2) -> 数据逆序的情况下 5 4 3 2 1
* 空间复杂度:O(1)
* 稳定性:稳定的排序
* 当数据越有序的时候 直接插入排序的效率越高
*/
public static void insertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int j = i - 1;
int temp = array[i];
while (j >= 0 && temp < array[j]) {
array[j + 1] = array[j];
array[j] = temp;
j--;
}
}
}
希尔排序
希尔排序可以看作是插入排序的一种优化
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成多个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
/**
* 希尔排序
* 每组进行插入排序
* 时间复杂度:O(n^1.25 - n^1.5) -> n^1.3不确定
* 空间复杂度:O(1);
* 稳定性:不稳定;
*/
private static void shell(int[] array, int gap) {
//插入排序
for (int i = gap; i < array.length; i++) {
int temp = array[i];
int j = i - gap;
while (j >= 0 && temp < array[j]) {
array[j + gap] = array[j];
array[j] = temp;
j -= gap;
}
}
}
//分组操作
public static void shellSort(int[] array) {
//分组操作
int gap = array.length;
while (gap > 1) {
gap /= 2;
shell(array, gap);
}
}
图片:
选择排序
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
/**
* 选择排序
* 时间复杂度:O(N*logN) 对数据不敏感 无论数据原本是有序还是无序 都是这个表达式
* 空间复杂度:O(1)
* 稳定性:不稳定
*/
public static void selectSort1(int[] array) {
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i + 1; j < array.length; j++) {
if (array[j] < array[minIndex]) {
minIndex = j;
}
}
swap(array, minIndex, i);
}
}
优化 可以再一次遍历中找到最大的和最小的这样效率可以快一倍
public static void selectSort2(int[] array) {
int left = 0;
int right = array.length - 1;
while (left < right) {
int minIndex = left;
int maxIndex = right;
for (int i = left + 1; i <= right; i++) {
if (array[i] < array[minIndex]) {
minIndex = i;
}
if (array[i] > array[maxIndex]) {
maxIndex = i;
}
}
swap(array, minIndex, left);
//此时特殊处理首元素是最大的元素
if (maxIndex == left) {
maxIndex = minIndex;
}
swap(array, maxIndex, right);
left++;
right--;
}
}
堆排序
利用堆的性质 如果从小到大排序使用大根堆 如果从大到小排序 使用小根堆
大根堆的堆顶元素是最大的 将这个元素和最后一个元素交换 此时最后一个元素确定位置 隔离最后一个元素 向下调整堆 重复上述操作 即将元素从小到大排序
/**
* 堆排序
* 时间复杂度 O(N*logN)对数据不敏感 不管有序无序都是这个表达式
* 空间复杂度 O(1)
* 稳定性 不稳定排序
*/
public static void heapSort(int[] array) {
createBigHeap(array);
//构建大根堆
int end = array.length - 1;
//标记末尾位置
while (end > 0) {
swap(array, 0, end);
shiftDown(array, 0, end);
//此时向下调整不包含最后一个元素
end--;
}
}
private static void createBigHeap(int[] array) {
for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
shiftDown(array, i, array.length);
}
}
private static void shiftDown(int[] array, int parent, int len) {
int child = 2 * parent + 1;
while (child < len) {
if (child + 1 < len && array[child] < array[child + 1]) {
child = child + 1;
}
if (array[child] > array[parent]) {
swap(array, parent, child);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
冒泡排序
**相邻元素两两比较交换 **
/**
* 冒泡排序
* 时间复杂度:O(N^2) 对数据不敏感 有序 无序都是这个复杂度!
* 空间负责度:O(1)
* 稳定性:稳定的排序
* 加了优化之后,时间复杂度可能会变成O(n)
*/
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 - 1 - i; j++) {
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
flag = true;
}
}
if (!flag) {
//如果没有进行swap操作 说明数据已经有序
break;
}
}
}
快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
第一步: **找到基准值 **
第二步: 将大于基准值的元素放在基准值后 小于基准值的元素放在基准值之前
序列的分割
我们在找到基准值key后 要将大于key的数据放在key之后 小于key的元素放在key之前 那么我们应该如何进行操作呢?
Hoare法
这个方法的基本思想是**定义一个前后指针left和right 一个指向序列头 一个指向序列尾 指针移动 当right指针指向小于key值的元素时停下 left指针指向大于key值时停下 此时让left指针和right指针指向的元素交换 知道left指针遇到right指针时停下 **
public static int parttion1(int[] array, int left, int right) {
int key = left;
int temp = array[left];
while (left < right) {
while (left < right && array[right] >= temp) {
right--;
}
while (left < right && array[left] <= temp) {
left++;
}
swap(array, left, right);
}
swap(array, key, left);
return left;
}
挖坑法
挖坑法的基本思路是还是定义前后指针left和right 一个指向序列头 一个指向序列尾 记录下left指向的元素(将此处看作一个坑) 移动right元素 直到遇到小于key值的元素 此时将right指向的元素放在坑中 此时这个位置就变成了一个坑 在移动left指针 直到遇到大于key的元素 此时再把left指向的元素放在坑中 直到left和right相遇 最后把key值放在最后的坑中
//2.挖坑法
public static int parttion2(int[] array, int left, int right) {
int temp = array[left];
//先在第一个位置挖一个坑 temp保存这个坑的元素
while (left < right) {
while (left < right && array[right] >= temp) {
right--;
}
array[left] = array[right];//把小的放进坑里 此时right位置就是另一个坑
while (left < right && array[left] <= temp) {
left++;
}
array[right] = array[left];
}
array[left] = temp;//把最后一个坑填起来
return left;
}
快慢指针法
前后指针法的核心思想是用一对快慢指针维护一个小于key的区间,有点滑动窗口的意味在里面。虽然代码量是最短的,但是理解起来也并不困难
fast一直往后遍历,当fast遇到小于key的元素时,则将fast处的元素与slow+1处的元素进行交换
为什么是slow + 1处的元素呢?因为pre以内的数据(除了头为key)都满足小于等于key的条件
当cur遍历结束时pre左边的数据都是小于等于key的,右边都是大于等于key的。最后还需要做的操作是将pre处的数据与left处的数据进行交换,这样就确定了key应该在的位置
public static int parttion3(int[] array, int left, int right) {
int slow = left;
int fast = left + 1;
//定义快慢指针 并初始化
while (fast <= right) {
//当快指针为最后一个元素时循环结束
if (array[fast] < array[left] && array[++slow] != array[fast]) {
//第一个条件保证快指针fast指向的元素小于我们的key元素
//第二个条件和++slow交换是因为 slow之前的元素都是小于key的
//当第一个条件成立时 第二个条件不执行 也就是在这次循环中 slow不进行++操作
//当两个条件都成立时 此时slow的下一个元素 就是一个大于key的元素 但是这个元素还不可以是fast指向的元素
//即if条件的原因
swap(array, fast, slow);
//交换元素
}
fast++;
}
swap(array, left, slow);
//此时key的位置已经确定 即slow此时指向的位置
//因为slow之前的元素都是小于key slow之后的元素都是大于key
return slow;
}
优化1 - 三数取中
快排在操作时 如果元素本身有序 则会出现以下情况
我们每次找到的基准值都是这段序列中最小的元素 此时 基准值key在调整后 依旧是序列的第一个元素
此时我们递归基准值的左边和右边 此时 左边没有元素 只需要递归右边
如果我们这段序列有n个元素 那么我们就需要递归n次 则递归的深度就是O(N) 此时非常容易发生栈溢出
根据这个情况 我们可以采用三数取中的方法来减少递归次数
- 求出序列第一个元素 中间元素 最后一个元素的中间值
- 把这个中间值放在 序列的开头
- 在对序列进行调整 把大于key的元素放在key左边 小于k的元素放在k右边
**此时因为我们将中间大小的元素 放在了序列的开头 让元素基本平均放在key值的左右 此时就可以减少递归的次数 **
优化2- 数据规模小时的插入
我们的 快排的递归调用 可以看作是一颗二叉树 找到key并且调整完序列后 对key的左序列和右序列进行递归
此时我们可以想到越靠近数的叶子节点 递归的 数量越多 假如二叉树是一颗完全二叉树则
第一层1次
第二层2次
第三层4次
第四层8次
对于这种情况 我们可以对数据规模较小的数据采用插入排序来减少递归的调用 以此来完成对快排的优化
/**
* 时间复杂度:
* O(n*logN)[最好情况了] O(N^2)[数据是有序的 或者是逆序的 ]
*
* 空间复杂度:O(logN)[好的情况] O(n) [不好的情况]
* 稳定性:不稳定排序
*/
//划分元素方法
//1.hoare法
public static int parttion1(int[] array, int left, int right) {
int key = left;
int temp = array[left];
while (left < right) {
while (left < right && array[right] >= temp) {
right--;
}
while (left < right && array[left] <= temp) {
left++;
}
swap(array, left, right);
}
swap(array, key, left);
return left;
}
//2.挖坑法
public static int parttion2(int[] array, int left, int right) {
int temp = array[left];
//先在第一个位置挖一个坑 temp保存这个坑的元素
while (left < right) {
while (left < right && array[right] >= temp) {
right--;
}
array[left] = array[right];//把小的放进坑里 此时right位置就是另一个坑
while (left < right && array[left] <= temp) {
left++;
}
array[right] = array[left];
}
array[left] = temp;//把最后一个坑填起来
return left;
}
//3.双指针法
public static int parttion3(int[] array, int left, int right) {
int slow = left;
int fast = left + 1;
//定义快慢指针 并初始化
while (fast <= right) {
//当快指针为最后一个元素时循环结束
if (array[fast] < array[left] && array[++slow] != array[fast]) {
//第一个条件保证快指针fast指向的元素小于我们的key元素
//第二个条件和++slow交换是因为 slow之前的元素都是小于key的
//当第一个条件成立时 第二个条件不执行 也就是在这次循环中 slow不进行++操作
//当两个条件都成立时 此时slow的下一个元素 就是一个大于key的元素 但是这个元素还不可以是fast指向的元素
//即if条件的原因
swap(array, fast, slow);
//交换元素
}
fast++;
}
swap(array, left, slow);
//此时key的位置已经确定 即slow此时指向的位置
//因为slow之前的元素都是小于key slow之后的元素都是大于key
return slow;
}
/**
* 优化1:对key值的取法
* 三数取中法
*/
private static int threeNumber(int[] array, int left, int right) {
int mid = (left + right) / 2;
if (array[left] < array[right]) {
if (array[mid] < array[left]) {
return left;
} else if (array[mid] > array[right]) {
return right;
} else {
return mid;
}
} else {
if (array[mid] < array[right]) {
return right;
} else if (array[mid] > array[left]) {
return left;
} else {
return mid;
}
}
}
/**
* 优化2:对规模较小时的优化
* 对规模较小时可以采用插入排序来操作
*/
private static void insertSort2(int[] array, int left, int right) {
for (int i = left + 1; i <= right ; i++) {
int temp = array[i];
int j = i - 1;
while(j >= left && array[j] > temp){
array[j + 1] = array[j];
array[j] = temp;
j--;
}
}
}
private static void quick(int[] array, int start, int end) {
if (start >= end) {
return;
}
if(end - start + 1 <= 20){
insertSort2(array,start,end);
return;
}
int mid = threeNumber(array,start,end);
swap(array,mid,start);
int pivot = parttion2(array, start, end);
quick(array, start, pivot - 1);
quick(array, pivot + 1, end);
}
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
/**
* 非递归实现快排
*/
public static void quickSort1(int[] array){
Stack<Integer> stack = new Stack<>();
int start = 0;
int end = array.length - 1;
if(end - start <= 20){
//直接插入排序
insertSort2(array,start,end);
return;
}
//三数取中
int mid = threeNumber(array,start,end);
//把中间值交换到key位置
swap(array,mid,start);
int pivot = parttion1(array,start,end);
//已经排好序的一个元素
if(pivot > start + 1){
stack.push(start);
stack.push(pivot - 1);
}
}
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
/**
* 归并排序
* 时间复杂度:O(N*logN)
* 空间复杂度:O(N)
* 稳定性:稳定的排序
*/
private static void merge(int[] array, int left,int mid, int right){
int s1 = left;
int e1 = mid;
int s2 = mid + 1;
int e2 = right;
int[] tempArr = new int[right - left + 1];
int k = 0;
//记录tempArr数组的下标
while (s1 <= e1 && s2 <= e2){
if(array[s1] <= array[s2]){
tempArr[k++] = array[s1++];
}else{
tempArr[k++] = array[s2++];
}
}
while (s1 <= e1){
tempArr[k++] = array[s1++];
}
while (s2 <= e2){
tempArr[k++] = array[s2++];
}
for (int i = 0; i < k; i++) {
array[i + left] = tempArr[i];
}
}
private static void mergeSortFunc(int[] array, int left, int right){
if(left >= right){
return;
}
int mid = (left + right) / 2;
mergeSortFunc(array,left,mid);
mergeSortFunc(array,mid + 1,right);
//合并
merge(array,left,mid,right);
}