一、排序的稳定性
两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则称该算法是具备稳定性的排序算法
二、七大基于比较的排序—原地排序
1、插入排序—此处以升序为例
给定一个无序数组,将其分为已排序区间和待排序区间(初始情况下整个数组都为待排序区间,已排序区间为空) ,一般可将第一个元素看作是在已排序区间内
//1、插入排序
//原地排序
public static void insertSort(int[] arr){
//设定一个bound标记,用来区分已排序区间和待排序区间
//[0,bound)是已排序区间
//[bound,length)是待排序区间
int bound=1;
//外层循环是更新待排序区间和已排序区间
for(;bound<arr.length;bound++){
//执行每次比较插入的过程
//创建一个引用v指向待排序区间最开始的元素
int v=arr[bound];
int cur=bound-1;
//内循环是实现比较,将v与其前一个元素(已排序区间最后一个元素)进行比较
for(;cur>=0;cur--){
//如果v比前一个元素小,则应把v插入到arr[cur]之前
//把cur的位置往后搬运一个格子(画图理解)
if(arr[cur]>v){
arr[cur+1]=arr[cur];
}else{
//触发else即说明已经找到合适的位置(即cur的后面位置)了,循环结束(画图理解)
break;
}
}
//找到合适的位置之后,将v插入该位置
arr[cur+1]=v;
}
}
- 注意:插入排序是一个稳定排序,但一定要注意代码实现细节(不能是>=)
2)时间复杂度:O(N²)
空间复杂度:O(1)–(额外开辟的空间)
3**)插入排序的两个特点:**
…如果数组长度比较短,排序效率很高
…如果数组相对有序,排序效率也很高
二、希尔排序—依据插入排序的两个特点进行的改进
-
排序过程:针对序列进行分组,对每一组分别进行插入排序,随时调整分组的变化,逐渐使整个数组逼近有序的状态
-
使用间隔系数gap来作为分组的依据
eg:令gap=3,即下标差值为3的元素被分到一组
再令gap=2分组…
再令gap=1,此时的希尔排序完全等价于插入排序 -
前面的分组(组数多但长度短,效率高)+排序做好了准备工作,让原本的数组更加接近有序数组,使最后一步插入排序的效率更高
-
实际选择时的常见序列:size/2,size/4,size/8…1 ------->希尔序列
-
时间复杂度:O(N²)
空间复杂度:O(1)
稳定性:不稳定
注意:并不是针对每个组分别进行插排,而是交替进行,先处理第0组的第一个数据,再处理第一组的第一个数据…处理第0组第二个数据,第一组第二个数据…
//希尔排序
public static void shellSort(int[] arr){
//先设定一个gap值作为分组依据
//希尔序列:size/2,size/4,size/8...1
int gap=arr.length/2;
while(gap>=1){
//设定一个辅助方法,进行插排
_shellSort(arr,gap);
gap=gap/2;
}
}
public static void _shellSort(int[] arr, int gap) {
//设定一个边界值bound,用来区分待排序区间和已排序区间
int bound=gap;
for(;bound<arr.length;bound++){
int v=arr[bound];
int cur=bound-gap;
for(;cur>=0;cur-=gap){
if(v<arr[cur]){
//满足条件,搬运
arr[cur+gap]=arr[cur];
}else{
//触发else即说明已经找到合适的位置,跳出循环
break;
}
}
找到合适的位置之后,将v插入该位置
arr[cur+gap]=v;
}
}
三、选择排序
-
实现思路:
先将整个数组分为两个区间(待排序区间和已排序区间),遍历待排序区间,通过打擂台的方式,找出这个区间中最小的元素(以待排序区间的开始位置作为擂台),则擂台位置的元素就相当于在已排序区间中了 -
时间复杂度:O(N²)
空间复杂度:O(1)
稳定性:不稳定
//选择排序
public static void select Sort(int[] arr){
//设定一个bound标记,用来区分已排序区间和待排序区间
//[0,bound)是已排序区间
//[bound,length)是待排序区间
//待排序区间起始位置为擂台(找出最小元素)
int bound=0;
for(;bound<arr.length;bound++){
for(int cur=bound+1;bound<arr.length;cur++){
if(arr[cur]>arr[bound]){
//如果挑战者比擂主小,交换两个元素
swap(arr,cur,bound);
}
}
}
}
public static void swap(int[] arr,int x,int y){
int tmp=arr[x];
arr[x]=arr[y];
arr[y]=tmp;
}
四、堆排序—本质上是一种优化的选择排序
- 直观上的理解:如果想进行升序排序,可以建立一个小堆,每次取出堆顶元素,依次取N次,放入一个新的数组中,就可的到一个升序数组,但是,我们此处研究的是原地排序,因此,该方法不可用
- 基于原地排序的前提下:
我们可以建立一个大堆,每次取出堆顶元素,让其和堆的最后一个元素进行交换,交换完成之后将最后一个元素删掉(此处的删除只是将其从堆中删除,而不是从数组中删除,即size–),要此时,这个最大值虽然从堆上删除了,但是就正好来到了这个数组的末尾。然后从0号元素进行向下调整(注意不是从最后一个非叶子节点调整), 使前面的元素重新称为堆(不算数组最末尾的元素,因为删除堆顶元素时要进行size–),再将0号元素和堆的最后一个元素进行交换(就相当于此时的0号元素来到了数组上的倒数第二个位置上了),重复上述过程
//堆排序
public static void heapSort(int[] arr){
//先建立一个堆
createHeap(arr);
int heapSize=arr.length; //堆的大小
//循环取出堆顶元素,让其和堆的最后一个元素进行交换
for(int i=0;i<arr.length;i++){
swap(arr,0,heapSize-1);
//交换完成之后,将最后一个元素从堆中删掉
heapSize--;
//从0号元素开始进行向下调整
shiftDown(arr,heapSize,0);
}
}
//向下调整
public static void shiftDown(int[] arr, int heapSize, int index) {
int parent=index;
int child=2*parent+1;
while(child<heapSize){
//找出左右子树中比较大的
if(child+1<heapSize&&arr[child+1]>arr[child]){
//让child指向较大的元素
child=child+1;
}
//再取比较child和parent
if(arr[parent]<arr[child]){
swap(arr,parent,child);
}else{
break;
}
//更新循环变量
parent=child;
child=2*parent+1;
}
}
//建堆
public static void createHeap(int[] arr) {
//从数组的最后一个非叶子节点开始进行向下调整
for(int i=(arr.length-1-1)/2;i>=0;i--){
shiftDown(arr,arr.length,i);
}
}
五、冒泡排序
- 时间复杂度:O(N²)
- 空间复杂度:o(1)
- 稳定性:稳定
//冒泡排序--->此处以升序排序为例(从后往前遍历)
public static void bubbleSort(int[] arr){
//设定一个bound标记,用来区分已排序区间和待排序区间
//[0,bound)是已排序区间
//[bound,length)是待排序区间
int bound=0;
for(;bound<arr.length;bound++){
for(int cur=arr.length-1;cur>bound;cur--){
if(arr[cur]<arr[cur-1]){
swap(arr,cur,cur-1);
}
}
}
}
public static void swap(int[] arr,int x,int y){
int tmp=arr[x];
arr[x]=arr[y];
arr[y]=tmp;
}
六、快速排序
-核心操作:partition:先在待排序数组中选取一个**“基准值”**(一般取第一个元素或者最后一个元素作为基准值),然后把这个数组整理成左侧比基准值小,右侧比基准值的的形式
- 调整方式:使用左右下标从两边往中间走的方式来实现—>从左往右找一个比基准值大的数,从右往左找一个比基准值小的数,交换这两个数,重复上述过程,直到left于right重合,此时整理完毕,将重合位置元素与基准值进行交换,就会得到一个以基准值为中心,左侧比基准值小,右侧比基准值大的数组,然后递归地将基准值两侧的两个区间进行partition操作
//快速排序
public static void quickSort(int[] arr) {
//创建一个辅助递归方法,明确指定针对哪个区间进行递归
//[0,length-1]
_quickSort(arr, 0, arr.length - 1);
}
public static void _quickSort(int[] arr, int left, int right) {
while (left >= right) {
return;
}
//符合条件时,进行partition操作,将基准值保存在一个变量中(即左右两侧区间分割线)
int index = partition(arr, left, right);
//对基准值左右两侧区间进行递归(快速排序)
_quickSort(arr, left, index - 1);
_quickSort(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) {
//先从左往右找一个比基准值大的数字
//循环结束时,l就指向了一个比基准值大的数字
while(l<r&&arr[l]<=v){
l++;
}
//再从右往左找一个比基准值小的数字
//循环结束时,r就指向了一个比基准值小的数字
while(l<r&&arr[r]>=v){
r--;
}
//找到之后,交换两个元素
swap(arr,l,r);
}
//当l和r重合时,交换重合元素和基准值
swap(arr,l,right);
//返回基准值的位置
return l;
}
- 注意:如果找最左侧元素为基准值,那就要先从右往左找比基准值小的元素,再从左往右找比基准值大的元素,然后交换(先后顺序很重要)
- 时间复杂度
平均:O(NlogN)—>每次选的基准值都是当前区间中比较接近中位数的数组
最坏:O(N²)—>每次选的基准值都是最大/最小值
- 空间复杂度(取决于递归深度)
平均:O(logN)
最坏:O(N)
- 稳定性:不稳定
- 快速排序的优化手段
1、三数取中:找到第一个元素、最后一个元素和中间位置元素,看三者哪一个更接近于中间值
2、当待处理区间已经比骄小时,就不再进行递归了,直接针对该区间进行插入排序
3、当递归深度达到一定深度,并且当前待处理区间还是比较大时,还可以使用堆排序