七大排序算法(Java)
作者:互联网
目录
排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
常见排序:
插入排序
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
我们假设第一位数是有序的,当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素向后移。
而这其中的插入又有两种情况,那就是插入的数据在在中间及在最前面。
当我们插入最后一个元素时,前n-1个元素已经排好序。
代码实现:
class InsertSort{
public static void InsertSort(int []arr) {
for (int i = 0; i < arr.length - 1; i++) {
int end = i;
int tmp = arr[end + 1];
while (end >= 0) {
if (arr[end] > tmp) {
arr[end + 1] = arr[end];
end--;
} else {
break;
}
}
arr[end + 1] = tmp;
}
System.out.println(Arrays.toString(arr));
}
}
public class Test {
public static void main(String[] args) {
int arr[] = {10,24,5,25,39,46,43};
InsertSort.InsertSort(arr);
}
}
时间复杂度分析:最坏情况下也就是逆序,移动次数为1+2+3+...+n-1,即等差数列求和为O(n^2);
空间复杂度分析:O(1);
稳定性:稳定
希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数(gap),把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
希尔排序其实是对插入排序的优化,当我们面临逆序的数组排序,我们先通过gap分组进行预排序,使数组尽量变为有序。最后在通过插入排序排序完成。
我们发现预排序其实就是几次插入排序,但如果我们分开去插入排序,效率会很低,于是我们还是按照顺序依次来进行插入排序,但插入排序的下标却是他们已经分好组的0,1,2下标。那怎么确定他们是哪个组呢?加个gap不就好了。
end从0开始,end+gap的是一组,分好的组进行插入排序。
ps:gap越大,排序速度越快,但排好后的数组越不接近有序,gap越小,排序越接近有序。我们一般将希尔排序的gap每次除以2,应为要保证最后一次的gap要等于1,就是说最后一次一定是插入排序,或者将gap除以3在最后加上1,也能保证gap的最后结果是1.
代码实现
class ShellSort{
public static void ShellSort(int []arr){
int gap = arr.length;
while(gap >= 1) {
gap = gap/ 2;
for (int i = 0; i < arr.length - gap;i++) {
int end = i;
int tmp = arr[end + gap];
while (end >= 0) {
if (arr[end] > tmp) {
arr[end + gap] = arr[end];
end -= gap;
} else {
break;
}
}
arr[end+gap] = tmp;
}
}
System.out.println(Arrays.toString(arr));
}
}
public class Test {
public static void main(String[] args) {
int arr1[] = {9,8,7,6,5,4,3,2,1,0};
ShellSort.ShellSort(arr1);
}
}
时间复杂度分析:
在我们的算法中:
当gap很大时,每个数都至少移动一次,因此时间复杂度接近O(n)
当gap很小时,数组已经很接近有序,移动的次数也接近O(n)。
而当我们用的是gap/2时,要进行排序的次数x为以2为底指数为n的对数,logn。
因此整体时间复杂度是O(log N * N);
但在查阅书籍后发现希尔排序的时间复杂度为O(N ^1.3),具体如何而来我就不深究了,有兴趣的小伙伴可以去了解。
空间复杂度:0(1);
稳定性:不稳定
选择排序
思路:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换。在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。
排序太过简单,直接上代码。
代码实现
class SelectSort{
public static void SelectSort(int []arr){
for(int i = 0;i < arr.length - 1;i++){
int min = i;
for(int j = i + 1;j < arr.length ;j++){
if(arr[j] < arr[min]){
min = j;
}
}
if(min != i){
int tmp = arr[i];
arr[i] = arr[min];
arr[min] = tmp;
}
}
System.out.println(Arrays.toString(arr));
}
}
时间复杂度分析:O(n^2)
直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用.
空间复杂度:O(1)
稳定性:不稳定
堆排序
在知道堆排序之前我们需要知道什么是堆。
堆的逻辑结构是一颗完全二叉树,物理结构是一个数组。
我们能通过数组的下标表示父子的关系:
堆有两种表示结构:
大顶堆:父亲节点比所有的孩子节点的值都要大 ---找出最大值
小顶堆:父亲节点比所有的孩子节点的值都要小 ---找出最小值
那我们怎么通过堆来进行排序呢?
堆排序,首先肯定要建堆嘛,没堆怎么进行排序呢。所以我们第一步就是建堆。
给定一个数组,我们将它进行建堆,因为堆的逻辑结构是一个完全二叉树,所以建堆很简单吗?
注意:这可还不是一个堆,因为堆只有大顶堆和小顶堆,所以其实建堆很复杂的。
于是我们引入了一个向下调整算法。
向下调整算法:
前提:①升序:左右子树都是大堆②降序:左右子树都是小堆。
方法(按升序来讲):从根节点开始,从左右子树中选择一个较大的数与父节点进行比较,如果子节点比父节点大择交换。然后将子节点重新作为一个父节点继续往下调。
下图是是按降序来调整:
升序就是找大的,与这个相反。
我们能通过这个算法找到堆中的最大最小值,但这又什么用呢?而且它的前提也很难满足。
那我们不妨从底部开始向下调整,然后依次向上,最后这个堆不就是一个排好序的了嘛。
但最后一层都是只有一个元素,也就没有了调整的意义,那么从最后一个非叶子节点的位置开始调整。可如何找到这个位置呢?最后一个非叶子节点不就是最后一个叶子节点的父节点嘛,那这就很好求了,直接代入公式即可。
那么代码就很好实现了。下图是建大堆顶,小堆顶只需将大于改成小于即可。
当我们是升序排序时时需要建大堆的,因为如果建了小堆,我们找到了最小值在堆顶,最小值也就固定不能动了,那我们又需要重新建堆在找出一个最小值,这样的堆排序也就没有了效率可言。
而如果我们建立大堆,那么最大值在堆顶,我们只需将堆顶的元素与最后一个元素交换,在重新向下调整时将要调整的长度减一那么就又能找出次大的元素再交换到倒数第二个位置,然后一直调整直至数组变为一个升序的数组。
完整代码
class HeapSort{
public static void AdjustDown(int []arr,int root,int length){
int parent = root;
int child = parent * 2 + 1;
while(child < length){
if(child + 1 < length && arr[child + 1] > arr[child]){
child += 1;
}else {
if(arr[child] > arr[parent]){
int tmp = arr[parent];
arr[parent] = arr[child];
arr[child] = tmp;
parent = child;
child = parent * 2 + 1;
}else {
break;
}
}
}
}
public static void HeapSort(int []arr){
for(int i = (arr.length - 1 - 1) / 2;i >= 0;i--){//建堆
HeapSort.AdjustDown(arr,i,arr.length);
}
int length = arr.length -1;
while(length > 0){
int tmp = arr[0];
arr[0] = arr[length];
arr[length] = tmp;
HeapSort.AdjustDown(arr,0,length);
length --;
}
System.out.println(Arrays.toString(arr));
}
}
时间复杂度分析:
所以整体的时间复杂度就是次数最多的:堆排序再向下调整,也就是O(N * log N).
空间复杂度:O(1);
稳定性:不稳定
冒泡排序
冒泡排序也很好理解,将前一个数与后一个数进行比较,大的往后移,一直到最后,然后小的元素就会慢慢浮到数组顶端,因此就叫冒泡排序。
代码实现
class BubbleSort{
public static void BubbleSort(int []arr){
for(int i = 0;i < arr.length;i++){
for(int j = 1;j < arr.length - i;j++){
if(arr[j-1] > arr[j]){
int tmp = arr[j-1];
arr[j-1] = arr[j];
arr[j] = tmp;
}
}
}
System.out.println(Arrays.toString(arr));
}
}
时间复杂度分析:需要排序n-1趟,分别是n,n-1,n-2……1
总的次数为n(n-1)/2,即 O(n^2)
空间复杂度:O(1);
稳定性:稳定
快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快速排序有许多种实现方式
第一种:挖坑法
挖坑法快排
给定一个数组。
我们在最左或最右处选择一个位置作为坑(pivot)。将坑位置的值赋给一个key,如果坑在左,则从右边找key小的元素,如果坑在右,则从左边找比key大的元素。找到了,将坑该位置作为一个新的坑,该元素的值赋给原来的坑位置。
通过挖坑法我们将大于key值与小于key值分在了坑的左右两侧,那么坑位置的值是排好序的,我们只需再将坑左右两边的值再排序就好了。怎么做呢?递归就好了,在左边位置的元素重新再挖坑排序,右边的也挖坑排序,直至左右两边都只分成了一个元素,那么该元素就已经排好序了。
递归代码实现
class QuickSort1{//挖坑法
public static void QuickSort(int []arr,int left,int right){
if(left >= right){
return;
}
int begin = left;
int end = right;
int pivot = begin;
int key = arr[begin];
while(begin < end){
while(begin < end && arr[end] >= key){
end --;
}
arr[pivot] = arr[end];
pivot = end;
while (begin < end && arr[begin] <= key){
begin++;
}
arr[pivot] = arr[begin];
pivot = begin;
}
pivot = begin;
arr[pivot] = key;
QuickSort(arr,left,pivot-1);
QuickSort(arr,pivot+1,right);
}
}
时间复杂度分析:
拓展:
挖坑法优化①:三数取中
三数取中:因为当数组接近有序时,快排的时间复杂度达到O(n^2),为了防止这种情况的出现,有人想到了一个很妙的方法,三数取中,给定分划的数组区间中,取mid的下标为左区间和右区间之和除2,再从三个数中取出不是最大也不是最小的数作为key,然后再向下分。
代码实现:
class QuickSort2{//挖坑法三数取中 public static void Swap(int []arr,int x,int y){ int tmp = arr[x]; arr[x] = arr[y]; arr[y] = tmp; } public static int midIndex(int []arr,int i,int j){ int mid = (i + j) / 2; if(arr[i] < arr[mid]){ if(arr[mid] < arr[j]){ return mid; }else if(arr[i] > arr[j]){ return i; }else{ return j; } }else{ if(arr[j] < arr[mid]){ return mid; }else if(arr[j] > arr[i]){ return i; }else{ return j; } } } public static void quickSort(int []arr,int left,int right){ if(left >= right){ return; } int Index = midIndex(arr,left,right); int begin = left; int end = right; Swap(arr,begin,Index); int pivot = begin; int key = arr[begin]; while(begin < end){ while(begin < end && arr[end] >= key){ end --; } arr[pivot] = arr[end]; pivot = end; while (begin < end && arr[begin] <= key){ begin++; } arr[pivot] = arr[begin]; pivot = begin; } pivot = begin; arr[pivot] = key; quickSort(arr,left,pivot-1); quickSort(arr,pivot+1,right); } }
挖坑法优化②:小区间优化
小区间优化:但数据量很大时,底下几层占用递归次数太多浪费了效率,尽管对cpu来说这些并不算什么,但我们还是希望能将算法优化。
那我们不妨在当左右区间长度小于10时使用其他的排序代替递归,大于10该递归还是让他继续递归,那么这算法又能被优化了。那么用哪个排序呢?我们推荐使用插入排序,因为除了插入排序能在较小数据排的较快,其他的都不太适合,希尔还要经过预排序,堆还要建堆,冒泡和选择就更别说了。所以我们使用插入排序。
代码实现
class QuickSort3{ public static void Swap(int []arr,int x,int y){ int tmp = arr[x]; arr[x] = arr[y]; arr[y] = tmp; } public static void InsertSort(int []arr,int left,int right) {//对插入排序进行了一丢丢变形 for (int i = left; i < right; i++) { int end = i; int tmp = arr[end + 1]; while (end >= 0) { if (arr[end] > tmp) { arr[end + 1] = arr[end]; end--; } else { break; } } arr[end + 1] = tmp; } } public static int midIndex(int []arr,int i,int j){ int mid = (i + j) / 2; if(arr[i] < arr[mid]){ if(arr[mid] < arr[j]){ return mid; }else if(arr[i] > arr[j]){ return i; }else{ return j; } }else{ if(arr[j] < arr[mid]){ return mid; }else if(arr[j] > arr[i]){ return i; }else{ return j; } } } public static void quickSort3(int []arr,int left,int right){ if(left >= right){ return; } int Index = midIndex(arr,left,right); int begin = left; int end = right; Swap(arr,begin,Index); int pivot = begin; int key = arr[begin]; while(begin < end){ while(begin < end && arr[end] >= key){ end --; } arr[pivot] = arr[end]; pivot = end; while (begin < end && arr[begin] <= key){ begin++; } arr[pivot] = arr[begin]; pivot = begin; } pivot = begin; arr[pivot] = key; if(pivot - 1 - left > 10){ quickSort3(arr,left,pivot-1); }else {//当左区间所有元素小于10时插入排序 InsertSort(arr,left,pivot - 1); } if(right - pivot - 1 >10) { quickSort3(arr, pivot + 1, right); }else{//当右区间所有元素小于10时插入排序 InsertSort(arr,pivot + 1,right); } } }
左右指针法快排
与挖坑法类似,只是表现形式不同,写过的函数在挖坑法里能找到。
代码实现
public static void partSort(int []arr,int left,int right){
if(left >= right){
return;
}
int Index = midIndex(arr,left,right);
Swap(arr,left,Index);
int begin = left;
int end = right;
int key = begin;
while(begin < end){
while(begin < end && arr[end] >= arr[key]){//找小
end--;
}
while(begin < end && arr[begin] <= arr[key]){//找大
begin++;
}
Swap(arr,begin,end);
}
Swap(arr,begin,key);
partSort(arr,left,begin-1);
partSort(arr,begin+1,right);
}
前后指针法
思路其实都差不多,只是一次排序的方式不同,后面的每次排序都是分治递归。
代码实现
public static void partSort2(int []arr,int left,int right){
if(left >= right){
return;
}
int Index = midIndex(arr,left,right);
Swap(arr,left,Index);
int prev = left;
int cur = left + 1;
int key = left;
while(cur <= right){
while(arr[cur] < arr[key] && ++prev != cur){
Swap(arr,prev,cur);
}
cur++;
}
Swap(arr,key,prev);
partSort2(arr,left,prev-1);
partSort2(arr,prev+1,right);
}
非递归快排
在我们之前的快排中都使用了递归,但其实这并不太好,因为递归有一个致命的缺陷:栈帧深度太深,栈空间不够用,会导致栈溢出。
举个例子:我们求1+2+3+……+n的值,用递归简单实现下
public static int f(int n){
return n == 1 ? 1 : f(n-1)+n;
}
10000次还行
10w次直接就栈溢出了。
因此在大多公司中都是使用的非递归算法。
而如何使用非递归呢?我们借用数据结构栈来模拟栈的递归。利用栈先进后出的特点实现。
先将前后指针法简单变形一下成为单趟的排序:
public static int singleSort(int []arr,int left,int right){//单趟快排
int begin = left;
int end = right;
int key = begin;
while(begin < end){
while(begin < end && arr[end] >= arr[key]){//找小
end--;
}
while(begin < end && arr[begin] <= arr[key]){//找大
begin++;
}
Swap(arr,begin,end);
}
Swap(arr,begin,key);//交换begin位置的值与key的值
return begin;
}
首先,建立一个栈,将数组的最后一个元素和第一个元素的下标入栈。则栈中的两元素就是要排序数组的区间。因为栈后进先出,所以先出的是第一个元素下标,用left接收,另一个则用right接收。然后调用单趟排序,将left,right给这个函数。最后这个单趟排序返回了一个下标key,这个下标的元素已经排好了。接下来就是判断key左区间是否超过两个或两个以上元素,超过则继续排。
因为我们希望先排左区间,所以我们先将右区间的第一个和最后一个元素下标压入栈中,再压左区间。最后实现了数组有序。
非递归代码实现
public static void quickSortNoR(int []arr){
Stack<Integer> stack = new Stack();//建立一个存整型的栈
stack.push(arr.length-1);//存入最后一个元素下标
stack.push(0);//存入第一元素下标
while(!stack.empty()){
int left = stack.pop();//取出栈顶元素
int right = stack.pop();//同上
int key = singleSort(arr,left,right);
if(key + 1 < right){//压入右区间元素下标
stack.push(right);
stack.push(key+1);
}
if(left < key - 1){//压入左区间元素下标
stack.push(key - 1);
stack.push(left);
}
}
stack.clear();//销毁栈
}
单趟排序也可以不使用左右指针法,也可以用挖坑法和前后指针法,简单变下型就OK。
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
代码实现
class MergeSort{
public static void merge(int []arr,int left,int mid,int right){
int []tmp=new int[arr.length];
int begin1 = left,end1 = mid;
int begin2 = mid+1,end2 = right;
int index = left;
while (begin1 <= mid && begin2 <= end2){
if(arr[begin1] < arr[begin2]){
tmp[index++] = arr[begin1++];
}else {
tmp[index++] = arr[begin2++];
}
}
while(begin1 <= end1){
tmp[index++] = arr[begin1++];
}
while(begin2 <= end2){
tmp[index++] = arr[begin2++];
}
for (int i = left; i <= right; i++) {
arr[i] = tmp[i];
}
}
public static void mergeSort(int []arr,int left,int right){
if(left >= right){
return;
}
int mid = (left+right) / 2;
mergeSort(arr,left,mid);//划分左区间
mergeSort(arr,mid+1,right);//划分右区间
merge(arr,left,mid,right);//合并
}
}
时间复杂度分析:merge复杂度为O(n),因为有两个循环加起来的长度为n。merge一共被调用了logN次,二分嘛,所以时间复杂度为O(N *logN);
空间复杂度:因为需要merge去构造数组,因此时间复杂度是O(N);
稳定性:稳定。
本文分享就到次结束了。
原创不易,记得三连哦!!!
标签:arr,right,Java,int,七大,end,排序,left 来源: https://blog.csdn.net/qq_59689127/article/details/121722015