====================================================================================================
排序的概念
- 排序:所谓排序,就是一连串数据/记录,经过一定规则或者根据某个或某些关键的大小,递增递减排列起来的操作;
- 稳定性:假定该数列/记录序列内部存在多个相同关键字/记录,经过排序,这些关键字/记录的相对次序保持不变;这样的排序算法具有稳定性;
- 内部排序:所有的数据元素都放在内存中进行排序;
- 外部排序:处理的数据过多,不能全部放入内存中处理,只能放入读写较慢的外部储存器-磁盘上;(外排序通常采用的是一种"排序- 归并 "的策略。 在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件。而后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。)
常见排序算法
插入排序
直接插入排序算法
- 直接插入排序算法:稳定性排序;
- 排序区间分为有序区间、无序区间 (初始以[0,1)为有序区间);
- 每次将无序区间第一个元素和有序区间各个元素进行比较,并插入到合适位置;
- 初始数组越有序,数组越短,插入时间效率越高;
- 时间复杂度O(n^2),空间复杂度O(1);
- 图解:
// 直接插入排序算法:
public void insertSort(int[] arr){
for(int bound=1;bound<arr.length;bound++){
int v=arr[bound];
int cur=bound-1;
for(;cur>=0&&arr[cur]>v;cur--){
arr[cur+1]=arr[cur]; //后挪
}
arr[cur+1]=v; // 插入合适位置
}
}
希尔排序算法
- 希尔排序/缩小增量法: 不是稳定的排序;
- 将需要排序的数组根据gap先分成若干份区间为gap的排序区间,分别进行插入排序;然后不断更新gap的值,使整个数组更加有序,最后gap==1时,等价于插入排序,最后将数据记录统一排好序;(初始一般 gap=arr.length/2;更新:gap/2)
- 希尔排序是当数组很长/非常无序时进行插入排序的优化算法;
- 时间复杂度最好O(n),平均O(n^1.3),最坏O(n ^2);空间复杂度:O(1);
- 图解:
// 希尔排序
public void shellSort(int[] arr){
int agp=arr.length/2;
while(gap>=1){
_shellSort(arr,gap);
gap/=2;
}
}
public void _shellSort(int[] arr,int gap){
int bound=gap;
for(;bound<arr.lemgth;bound++){
int v=arr[bound];
int cur=bound-gap;
for(;cur>=0 && arr[cur]>v;cur=cur-gap){
arr[cur+gap]=arr[cur];
}
arr[cur+gap]=v;
}
}
选择排序
选择排序算法
- 选择排序:打擂台的方式,每次将最大或最小元素放到最左边;与冒泡排序有点相似。
- 时间复杂度为O(n^2),空间复杂度O(1);
- 稳定性:不稳定;
// 选择排序
public void selectort(int[] arr){
for(int bound=0;bound<arr.length-1;bound++){
for(int cur=bound+1;cur<arr.length;cur++){
if(arr[bound]>arr[cur]){
int t=arr[bound];
arr[bound]=arr[cur];
arr[cur]=t;
}
}
}
}
堆排序算法
- 堆排序:基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大/最小的数,而是通过堆来选择无序区间的最大/最小的数;
- 排升序建大堆,排降序建小堆;
- 时间复杂度O(n*log(n)),空间复杂度O(1);
- 稳定性:不稳定;
- 图解:
//关于堆的相关操作:建堆与向下调整与向上调整可以查阅该跳转博文https://blog.csdn.net/weixin_47921628/article/details/121223625
// 堆排序
public void heapSort(int[] arr){
//初始建大堆
createHeap(arr);
//交换,排序
for(int heapSize=arr.length-1;heapSize>0;heapSize--){
swap(arr,0,heapSize);
shiftDown(arr,heapSize,0); //向下调整
}
}
//建堆方法:
public createHeap(int[] arr){
for(int index=(arr.length-1-1)*2;index>=0;index--){
shifDown(arr,arr.length,index);
}
}
//向下调整:
public void shifDown(int[] arr,int size,int index){
int parent=index;
int child=2*index+1;
while(child<size){
if(child+1<size&&arr[child]<arr[child+1]){
child=child+1;
}
if(arr[child]>arr[parent]){
int tmp=arr[parent];
arr[parent]=arr[child];
arr[child]=tmp;
}else{
break;
}
parent=child;
child=parent*2+1;
}
]
//交换方法:
public void swap(int[] arr,int a,int b){
int t=arr[a];
arr[a]=arr[b];
arr[b]=t;
}
交换排序
冒泡排序算法
- 冒泡排序:在无序区间,通过相邻数的比较,将最大/最小的数冒泡到无序区间的最后/最前,持续这个过程,直到数组整体有序;
- 时间复杂度最好O(n),平均O(n^2);空间复杂度O(1);
- 稳定性:稳定;
// 冒泡排序
public void bubbleSort(int[] arr){
for(int bound=0;bound<arr.length;bound++){
for(int cur=arr.length-1;cur>bound;cur--){
if(arr[cur]<arr[cur-1]){
int t=arr[cur];
arr[cur]=arr[cur-1];
arr[cur-1]=t;
}
}
}
}
*快速排序算法
- 快速排序算法:以排序范围里的 最右侧/最左侧 元素作为基准值,向中间遍历,(升序为例)前面比基准值大的与后面比基准值小的元素做交换,相遇时,当前元素与基准值交换;
- 从待排序区间选择一个数,作为基准值(pivot);
- Partition方法: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
- 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度为1,代表已经有序,或者小区间的长度为 0,代表没有数据;
- 时间复杂度O(nlog(n)),最坏O(n^2),空间复杂度最好/平均O(log(n)),最坏O(n);
- 稳定性:不稳定;
- 图解:
// 快速排序
public void quickSort(int[] arr){
//分治思想,递归方法
_quickSort(arr,0,arr.length-1);
}
//递归方法
public void _quickSort(int[] arr,int left,int right){
if(left>=right){
return ;
}
// 从两边遍历,找的隔断区间的元素位置(左区间总比基准值小,有区间总比基准值大)
int index=partition(arr,left,right);
_quickSort(arr,left,index-1);
_quickSort(arr,index+1,right);
}
//关键调整操作hoare方法,//以右侧元素为基准值
public int partition(int[] arr,int left,int right){
int pivot=arr[right]; //以右侧元素为基准值
int i=left;
int j=right;
while(i<j){
while(i<j&&arr[i]<=pivot){i++;}
while(i<j&&arr[j]>=pivot){j--;}
swap(arr,i,j);
}
swap(arr,i,right);
return i;
}
//交换方法:
public void swap(int[] arr,int a,int b){
int t=arr[a];
arr[a]=arr[b];
arr[b]=t;
}
//以区间最左侧为基准值,挖坑方法
public int partition(int[] arr,int left,int right){
int pivot=arr[left];
int i=left;
int j=right;
while(i<j){
while(i<j&&arr[j]>=pivot){j--;}
arr[i]=arr[j];
while(i<j&&arr[i]<=pivot){i++;}
arr[j]=arr[i];
}
arr[i]=pivot;
return i;
}
//前后遍历法,以左侧第一个元素为基准值
public int partition(int[] arr,int left,int right){
int d=left+1;
int pivot=arr[left];
for(int i=left+1;i<right;i++){
if(arr[i]<pivot){
swap(arr,i,d);
d++;
}
}
swap(arr,d-1,left);
return d-1;
}
基准值的选择:
- 选择边上(左或者右)
- 随机选择
- 几数取中(例如三数取中):array[left], array[mid], array[right] 大小是中间的为基准值
// 非递归方法实现:借助栈(后进先出)模拟,类似先序遍历
public void quickSort(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();
if (left >= right) {
continue;
}
int index = partition(arr, left, right);
stack.push(right);
stack.push(index + 1);
stack.push(index-1);
stack.push(left);
}
}
优化方法:
- 选择基准值很重要,通常使用几数取中法
- partition 过程中把和基准值相等的数也选择出来
- 待排序区间小于一个阈值(较小的值)时,使用直接插入排序
*归并排序
- 归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divideand Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
- 时间复杂度为O(nlog(n)),空间复杂度O(n);
- 稳定性:稳定;
- 图解
//归并排序
public void mergeSort(int[] arr){
//辅助方法,便于分治递归
mergeSortInternal(arr,0,arr.length);
}
public void mergeSortInternal(int[] arr,int left,int right){
if(left>=right-1){ //[left,right)此条件说明该区间只有一个元素,则返回不用归并排序;
return;
}
int mid=(left+right)/2;
//分治/分割
mergeSortInternal(arr,left,mid);
mergeSortInternal(arr,mid,right);
//归并-合并两个有序数组
merge(arr,left,mid,right);
}
//归并操作-合并两个有序数组
public void merge(int[] arr,int left,int mid,int right){
//先设置一个空数组,存取合并之后的元素
int[] tmp=new int[right-left];
int l=left;
int r=mid;
int size=0;
while (l < mid && r < right) {
if (arr[l] <= arr[r]) {
tmp[size++]=arr[l++];
}else{
tmp[size++]=arr[r++];
}
}
while (l<mid){
tmp[size++]=arr[l++];
}
while(r<right){
tmp[size++]=arr[r++];
}
//合并完之后将合并的数组赋值给原数组
while(size>=0&&right>left){
arr[right-1]=tmp[size-1];
size--;
right--;
}
}
海量数据排序处理
- 外部排序:排序过程需要在磁盘等外部存储进行的排序(前提:内存只有 1G,需要排序的数据有 100G)因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序:
- 先把文件切分成 200 份,每个 512 M
- 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
- 进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了
*数据与内存
- 单位换算:
- 1B(byte)=8(bit);
- 1KB=1024B;
- 1MB=1024KB;
- 1GB=1024MB;
- 1TB=1024GB;
- 存储单位和网速(每秒下载的字节数)的单位,不管是 B 还是 b,代表的都是 字节 Byte。(网速5KB-表示每秒下载5千字节的数据);
- 带宽(网络线路的计量单位)的单位(每秒接收的平均比特数bps->kbps(千)->(兆比特每秒)),不管是 B 还是 b,代表的都是 比特 bit 。(比如带宽2M-表示每秒接受2百万左右的比特数据);
- 一般 1million百万~=1M; 10亿 ~= 1G