起初是时间复杂度为O(n 2)级的三种排序算法:
一、冒泡排序
一边比较一边向后两两交换,将最大值/最小值,冒泡到最后一位。
public void bubbleSort(int []arr){
for(int i = 0; i < arr.length - 1; i++){
for(int j = 0; j < arr.length - 1 - i; j++){
if(arr[j] > arr[j + 1]){
//将大的换到右边
swap(arr, j, j + 1);
}
}
}
}
优化算法:如果外侧for循环的第i次遍历没有发生交换,则可以直接跳出循环(已经有序了)。
public void bubbleSort(int []arr){
boolean isSort = false;
for(int i = 0; i < arr.length - 1; i++){
//如果上次遍历后没有发生过交换,则代表数组已经有序
if(isSort == true){
break;
}
isSort = true;
for(int j = 0; j < arr.length - 1 - i; j++){
if(arr[j] > arr[j + 1]){
//将大的换到右边
swap(arr, j, j + 1);
//如果发生了交换,则不能代表已经有序
isSort = false;
}
}
}
}
复杂度分析:
时间复杂度:比较的次数为(n-1)+(n-2)+(n-3)+…+1 = n2/2,所以时间复杂度是O(n2);
空间复杂度:只用到i、j两个变量,空间复杂度只有O(1);
稳定性分析
因为array[j] = array[j + 1]时,可以不交换,所以是稳定的。
练习例题
将数组排成最小的数
将数组拼接成最小数字,本质上就是个排序问题!只不过要变换排序规则:新的规则为:判断两个数分别前后拼接后谁大谁小。
移动零
用冒泡的方法,将0放到尾部。新的规则为:遇见0就交换。
二、选择排序
双重循环遍历数组,经过一轮比较,找到最大的元素下标,将其交换到队尾
优点:可以实现局部有序
public void selectSort(int []arr){
for(int i = 0; i < arr.length; i++){
int max_index = 0;
for(int j = 0; j < arr.length - i; j++){
//从0开始,遍历更新最大值坐标
if(arr[j] > arr[max_index]){
max_index = j;
}
}
swap(arr, max_index, arr.length - 1 - i);
}
}
理解实例:
复杂度
时间复杂度:O(n2);
空间复杂度:O(1)
稳定性
不稳定,上述实例中第一次交换将arr[0]和arr[5]交换后,打乱了原本arr[4]=0和arr[5]=0的相对顺序,因此不稳定。
练习例题
数组中第k个最大元素
利用选择排序的优点,实现数组前部分局部有序。每次排序将最大值交换到开头,循环遍历k此后,返回数组中第k个元素即可。
三、插入排序
理解:打扑克时,一边抓牌一边给扑克牌排序,每次模一张牌,就将它插到手上已有的牌中的合适位置,逐渐完成整个排序。
插入方法:在新数字插入过程中,不断与前面的数字交换,直到找到适合自己的位置。
public static void insertSort(int[] arr) {
// 从第二个数开始,往前插入数字
for (int i = 1; i < arr.length; i++) {
// j 记录当前数字下标
int j = i;
// 当前数字比前一个数字小,则将当前数字与前一个数字交换
while (j >= 1 && arr[j] < arr[j - 1]) {
swap(arr, j, j - 1);
// 更新当前数字下标
j--;
}
}
}
整个过程就好像已经有一些人按身高(从低到高)排成了一排,这时一个新人想要插进去,他会从最后一个人开始对比身高,如果前面的人比他高,他就和前面的人换位置。直到换到前一个人比他低了为止。
复杂度分析
时间复杂度:还是两层循环,复杂度为:O(n2);
空间复杂度:只需要临时变量j,空间复杂度为O(1);
稳定性分析
稳定,每次插入后,向前交换时的判断条件中,如果相等了就停止了,不破坏相对顺序。
练习例题
对链表进行插入排列
原理上就是插入排序,只不过考验对单向链表操作的熟练度。
时间复杂度是O(nlogn)级排序算法:
四、堆排序
基本原理也是一种选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大数。
堆排序过程如下:
- 用数列构成一个大根堆,取出堆顶的数字;(根节点的值 ≥ 子节点的值,这样的堆被称之为最大堆,或大顶堆;)
- 调整剩余的数字,构建出新的大根堆,再次取出堆顶的数字;
- 循环往复,完成整个排序。
整体的思路如上,我们需要解决的问题有两个:
- 怎么用数列构建出一个大根堆;
- 取出堆顶的数字后,如何将剩余的数字调整成新的大根堆。
(1)构建大根堆
【思路】
- 将新加入的数,不断的向上换,且只用跟父节点进行比较(新的数下标index利用【(i - 1) / 2】计算出父节点的index,并父节点进行PK)
- 如果一直比父大,就一直往上换,直到不比此时的父大了或已经到头了,那就停止。
将此过程称之为heapInsert过程。
因此可以将数组从头开始每个元素进行一遍heapInsert过程,来构建大根堆。
【案例:heapInsert(int[] arr, int index), arr=[4,6,8,5,9], index = 4】
【heapInsert过程】(logn级别调整代价)
public static void heapInsert(int []arr, int index){
//当前的数如果大于父节点的数,就与父位置进行交换
while(arr[index] > arr[(index - 1) / 2]){
swap(arr, index, (index - 1) / 2);
//交换完后,index来到了父的位置,继续进行while判断
index = (index - 1) / 2;
//停止条件如上述思路讲解;
}
}
(2)调整堆
如果需要将大根堆中最大的数找出来,并且去掉,然后构建新的大根堆出来,此时应该怎么办?
- 给用户返回的最大值,一定是大根堆中index=0位置的数(根节点上);
- 拿去根节点后,将已经形成的堆结构最后的一个数放在index= 0位置上;
- 从头节点开始,先在该头节点左右孩子中选取一个最大节点,然后与它pk。如果小于该子节点了,就将头结点与此子节点值进行交换;
- 重复步骤3,直到此节点再也没有了子节点 或 此节点大于左右子节点中的最大值,就停止。
此过程称为:heapify
【实例】
【heapify过程】(logn级别调整代价)
//某个数在index位置,能够往下移动
public static void heapify(int []arr, int index, int heapSize){
int left = index * 2 + 1;//左孩纸的下标
while(left < heapSize){
//两个孩子,谁的值大,就把下标给largest
int largest = left + 1 < heapSize && arr[left] < arr[left + 1]? left + 1 : left;
//父和较大的孩子之间,谁的值大,把谁的下标给largest
largest = arr[largest] < arr[index] ? index : largest;
if(largest == index){
break;
}
swap(arr,largest,index);//交换值
index = largest;//交换值后,下标换到大于他的子节点下标
left = index * 2 + 1;//得到此时的左子节点
//进行下一轮判断
}
}
(3)堆排
public static void heapSort(int []arr){
if(arr == null || arr.length < 2){
return ;
}
//将数组大根堆化
for(int i = 0; i < arr.length; i++){//O(N)
heapInsert(arr,i);//O(log N)
}
//此时的长度
int heapSize = arr.length;
//交换头结点和末尾节点的值(堆区间的最大值),同时交换后尾结点的值在某种意义上已经不在算入我们的堆区间内,此时堆区间的长度-1;
swap(arr,0,--heapSize);
while(heapSize > 0){//O(N)
heapify(arr,0,heapSize);//O(log N)
swap(arr,0,--heapSize);//O(1)
}
}
复杂度分析
时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性分析:
不稳定。
练习例题
五、快速排序
- 从数组中抽出一个数(称之为基数(pivot))
- Partition:遍历整个待排序数组区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值右边;
- 采用分治思想,对左右两个小区间重复Partition,直到小区间的长度等于1或0
(1)快排1.0
过程实现
public void quickSort(int[] array){
quickSortInternal(array, 0, array.length - 1);
}
//[left,right]是待排序区间
public void quickSortInternal(int []array, int left, int right){
if(left == right){
return;
}
if(left > right){
return;
}
//选取第一个数作为基准值pivot
int pivotIndex = partition(array, left, right);
//[left,pivotIndex - 1]都是小于基准值的
//[pivotIndex + 1, right]都是大于基准值的
quickSortInternal(array,left, pivotIndex - 1);
quickSortInternal(array, pivotIndex + 1, right);
}
partition双指针分区算法原理
public int partition(int[] array, int left, int right){
int l = left;
int r = right;
int pivot = array[left];
//选择那一端开头值为pivot,就必须从另一端开始对比!
while(l < r){
while(l < r && array[r] >= pivot){
r--;
}
while(l < r && array[l] <= pivot){
l++;
}
swap(array,l, r);
}
swap(array, left, l);
return l;
}
(2)快排2.0
- 将数组通过pivot划分Wie三个位置:<pivot、 ==pivot、>pivot
每次只用处理小于和大于区间即可,减少了工作量- 用随机数进行pivot的选取。
需要三个下标:
- 小于区间的下标less = left - 1
- 大于区间的下标more = right
- 当前值的下标left
操作步骤:
- arr[right]作为pivot基准值
- 根据判断实时arr[left]和pivot值的大小来更新大小区间
-1. 如果当前值小于基准值:swap(arr, ++less, left++) 维护了小于、等于区间
-2. 如果当前值大于基准值:swap(arr, --more, left),left不往前是因为还需要判断交换过来的值,并且维护了大于区间
-3. 如果当前值等于基准值:left++,当前值下标直接往前,更新等于区间
public void quickSort(int[] array){
quickSortInternal(array, 0, array.length - 1);
}
public void quickSortInternal(int []arr, int left, int right){
if(left < right){
swap(arr, left + (int)(Math.random() * (right - left + 1)), right); //随机抽取数值
int[] p = partition(arr, left, right);
quickSortInternal(arr, left, p[0] - 1);
quickSortInternal(arr, p[1] + 1, right);
}
}
//这是一个处理array[left...right]的函数
//默认以array[right]做划分,pivot=arr[right], 分为三个区:<pivot ==pivot >pivot
//返回等于区域(左边界、右边界),所以返回一个长度为2的数组res,res[0],res[1]
public int[] partition(int[] array, int left, int right){
int less= left- 1; // <区间的右边界
int more = right; // >区间的左边界
int pivot = array[right];
while(left < more){ // left代表当前数的位置;array[right]代表划分值
if(array[left] < pivot){ // 当前数 < 划分值
swap(array, ++less, left++);
}else if(array[left] > pivot){ // 当前数 > 划分值
swap(array, --more, left);
}else{
left++;
}
}
swap(array, more, right);
return new int[]{less + 1, more};
}
public void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
性能分析
六、归并排序
利用归并思想实现:
- 先将数组递归分解;
- 再对分解后的两个数组进行比较合并(后序遍历)
public void mergeSort(int [] nums){
mergeSortInternal(nums, 0, nums.length - 1);
}
public void mergeSortInternal(int[] nums, int start, int end){
if(start >= end){
return;
}
int mid = (start + end) / 2;
mergeSortInternal(nums, start, mid);
mergeSortInternal(nums, mid + 1, end);
merge(nums, start, mid, end);
}
public void merge(int[] nums, int start, int mid, int end){
int[] res = new int[end - start + 1];
int i = start;
int j = mid + 1;
int index = 0;
while(i <= mid && j <= end){
if(nums[i] < nums[j]){
res[index++] = nums[i++];
}else{
res[index++] = nums[j++];
}
}
while(i <= mid){
res[index++] = nums[i++];
}
while(j <= end){
res[index++] = nums[j++];
}
for(int k = 0; k < res.length; k++){
nums[start + k] = res[k];
}
}
稳定性分析:
稳定