目录
一、冒泡排序
从左到右不断交换相邻逆序的元素,在一轮的循环之后,可以让未排序的最大元素上浮到右侧。
在一轮循环中,如果没有发生交换,就说明数组已经是有序的,此时可以直接退出。
package Review;
/*
*
* 稳定,时间复杂度O(n2),空间复杂度O(1)
*
* */
public class bubbleSort {
public static void bubbleSort(int[] arr) {
for(int i = arr.length-1; i >= 0; i--) {
for(int j = 0; j < i; j++) {
if(arr[j] > arr[j+1]) {
swap(arr, j, j+1);
}
}
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {10, 8, 1, 2, 5, 11, 20};
bubbleSort(arr);
for(int i : arr) {
System.out.print(i + " ");
}
}
}
二、选择排序
从待排序的数据中寻找最小值,将其与序列最左边的数字进行交换
1、思路:
首先,找到数组中最小的那个元素。
其次,将该数与数组的第一个元素交换位置(如果第一个元素是最小元素,则该元素自己与自己交换)。
接着,在剩下的元素中找到最小的元素。
如此往复,每次都是在剩余的元素中选择出最小者。
package codingTest3;
public class selectionSort {
public static void selectionSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// for(int i = 0; i < arr.length-1; i++) {
// int minIndex = i;
// for(int j = i+1; j < arr.length; j++) {
// minIndex = arr[j] < arr[minIndex] ? j : minIndex;
// }
// swap(arr, i, minIndex);
// }
// }
for(int i = 0; i < arr.length; i++) {
int minIndex = i;
for(int j = i+1; j < arr.length; j ++) {
if(arr[j] < arr[minIndex]) {//如果在剩余的数组中找到比当前元素小的数,则交换
swap(arr, j, minIndex);
}
}
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {3,1,2,4,5};
selectionSort(arr);
for(int a: arr) {
System.out.println(a);
}
}
}
三、插入排序
插入排序是一种从序列左端开始依次对数据进行排序的算法。在排序过程中,左侧的数据陆续归位,而右侧留下的就是还未被排序的数据。插入排序的思路就是从右侧的未排序区域内取出一个数据,然后将它插入到已排序区域内合适的位置上。
将每一张牌插入到其他已经有序的牌中的适当位置。为了给要插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。
在插入排序中,当前索引锁边所有的元素都是有序的,但是他们的最终位置还不确定,为了给更小的元素腾出空间,他们可能会被移动。当索引到达数组的右端时,数组的排序就完成了。
package Review;
/*
* 时间复杂度:O(N^2)
* 额外空间复杂度:O(1)
* 是否可实现稳定性:是
* 每次从未排好序的数列中拿第一个p,往已排好序的数列中插入,
* 插入的过程是从已排好序的数列末尾向前依次比较,
* 如果p比其小,则交换后继续往前比较,
* 否则,不交换,此次排序结束。类似摸扑克牌。
* */
public class insertSort {
public static void insertSort(int[] arr) {
if(arr == null || arr.length < 2) {
return;
}
for(int i = 1; i < arr.length; i++) {
for(int j = i-1; j >= 0; j--) {
if(arr[j] > arr[j+1]) {
swap(arr, j, j+1);
}
}
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {5,6,1,2,3,8};
insertSort(arr);
for(int num : arr) {
System.out.print(num + " ");
}
}
}
四、归并排序
归并排序算法会把序列分成长度相同的两个子序列,当无法继续往下分时(也就是每个子序列中只有一个数据时),就对子序列进行归并。归并指的是把两个排好序的子序列合并成一个有序序列。该操作会一直重复执行,直到所有子序列都归并为一个整体为止。
1、归并操作:
将两个有序数组归并成为一个更大的有序数组。
要将一个大 的数组进行排序,可以(递归)的将它分为两半分别排序,最后将结果归并起来。
2、特点:
优点:可以保证任意长度为N的数组排序的是按和NlogN成正比。
缺点:它所需的额外空间与N成正比
先左边排好序,再右边排好序,最后用外排的方法再排序。
package codingTest3;
/*
* 1、归并操作:
* 将两个有序数组归并成为一个更大的有序数组。
* 要将一个大的数组进行排序,可以(递归)的将它分为两半分别排序,最后将结果归并起来。
* 2、特点:
* 优点:可以保证任意长度为N的数组排序的是按和NlogN成正比。
* 缺点:它所需的额外空间与N成正比
* 先左边排好序,再右边排好序,最后用外排的方法再排序。
*
* 首先划分划分划分,一直划分到不能划分,即每个组都只有一个数值。
* 然后合并,合并的过程就是每个二划分排序的过程。
* 在合并的时候,开辟一个辅助数组,其大小等于这两个合并数列的大小。
* 设置两个指针分别指向每个数列的首部,然后比较得到其中较小的值,并将这个值放入辅助数组中。然后取出小值的那个数列的指针可以继续向前走,与另一个数列的指针所指向的值继续比较
* 这样比较完成后,如果两个数列中有个数列的数值有剩余,即其指针没有走到末尾,则将这个数列直接赋到辅助数组末尾即可。
* 然后将辅助数组中的值拷贝回原数组中刚才合并的那两个数列的位置上。
*
* */
public interface mergeSort {
public static void mergeSort(int[] arr) {
if(arr == null || arr.length < 2) {
return;
}
mergeSort(arr, 0, arr.length - 1);
}
public static void mergeSort(int[] arr, int l, int r) {
//递归的基准(base case)
if(l == r) {
return;
}
int mid = l + ((r-l) >> 1);//int mid = l + (r-1)/2;
//分,将一个大问题分成小的问题。(递归)
mergeSort(arr, l, mid);
mergeSort(arr, mid+1, r);
//治,将两个子问题的解合并可以得到较大问题的解。(合并)
merge(arr, l, mid, r);
}
public static void merge(int[] arr, int l, int mid, int r) {
int[] help = new int[r-l+1];//第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
//第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
int i = 0;//临时数组指针
int p1 = l;//第一个指针
int p2 = mid + 1;//第二个指针
//第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
while(p1 <= mid && p2 <= r) {
help[i++] = arr[p1] < arr[p2]? arr[p1++] : arr[p2++];
}
//比完之后,假如第1个有序区仍有剩余,则直接全部复制到 temp 数组
while(p1 <= mid) {
help[i++] = arr[p1++];
}
//比完之后,假如第2个有序区仍有剩余,则直接全部复制到 temp 数组
while(p2 <= r) {
help[i++] = arr[p2++];
}
//将help中的元素全部拷贝到原数组中
//(原left-right范围的内容被复制回原数组)
//for(i = 0; i < help.length; i++) {
// arr[l+i] = help[i];
//}
//也可以写成这个。就是把辅助数组中的全部搬到原数组中。
i = 0;
while(l <= r) {
arr[l++] = help[i++];
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {3,1,2,4,5};
mergeSort(arr);
for(int a: arr) {
System.out.println(a);
}
}
}
五、快速排序
快速排序算法首先会在序列中随机选择一个基准值(pivot),然后将除了基准值以外的数分为“比基准值小的数”和“比基准值大的数”这两个类别,再将其排列成以下形式。
[ 比基准值小的数 ] 基准值 [ 比基准值大的数 ]
接着,对两个“ [ ]” 中的数据进行排序之后,整体的排序便完成了。对“ [ ]” 里面的数据进行排序时同样也会使用快速排序。
快速排序,采用“分而治之”的思想处理问题。
我们这里讲一下三路快排:
它适用于含有大量重复元素的数组,时间复杂度在N~NlogN之间。
首先,选取一个基准元素,将数组切分为三个子数组,其中左子数组小于切分元素,中间数组等于切分元素,右子树组大于切分元素。
然后,再将这些子数组进行排序,那么整个数组就排序好了。
package codingTest3;
/*
* 快速排序,采用“分而治之”的思想处理问题。
* 我们这里讲一下三路快排:
* 它适合用于含有大量重复元素的数组,时间复杂度在N~NlogN之间。
* 首先,选取一个基准元素,将数组切分为三个子数组,其中左子数组小于切分元素,中间数组等于切分元素,右子树组大于切分元素。
* 然后,再将这些子数组进行排序,那么整个数组就排序好了。
* */
public class quickSort {
private static void quickSort(int[] arr) {
if(arr == null || arr.length < 2) {
return;
}
quickSort(arr, 0, arr.length-1);
}
private static void quickSort(int[] arr, int l, int r) {
if(l < r) {
//随机快排就是经典快排的基础上多这一步:产生一个随机位置与最后一个位置的数交
// 随机取需要排序的数组中的一个元素和数组的最后一个元素交换,作为划分值
//Math.random()返回一个double类型的0.0-1.0之间的数值,包括0,不包括1
//Math.random() * (r - l + 1)就是返回0到r-l+1(不包括)的范围的数
swap(arr, l + (int) (Math.random() * (r - l + 1)), r);
// 得到数组元素中等于划分值的区域
int[] p = partition(arr, l, r);
// 小于等于划分值的区域 从
quickSort(arr, l, p[0]-1);
// 大于划分值的区域
quickSort(arr, p[1]+1, r);
}
}
private static int[] partition(int[] arr, int l, int r) {
// 初始化小于等于划分值区域的当前下标,默认是数组第一个元素的前一个位置
int less = l-1;
// 初始化大于划分值区域的当前下标,默认是数组最后一个元素的位置,同时也是划分值的位置,
//但该值并不属于大于划分值的区域,所以要在最后进行移动
int more = r;
// 当前下标小于大于划分值区域的下标时
while (l < more) {
// 当前值比划分值小,当前值和小于等于划分值区域的右边第一个值进行交换,
//小于等于划分值区域右移1个下标,当前下标+1
if (arr[l] < arr[r]) {
swap(arr, ++less, l++);
// 当前值比划分值大,当前值和大于划分值区域的左边第一个值进行交换,大于划分值的区域左移1个下标
} else if (arr[l] > arr[r]) {
swap(arr, --more, l);
// 当前值等于划分值,当前下标+1
} else {
// 当前下标+1
l++;
}
}
//当两个指针相遇的时候,还要将切分元素(这里是最后一个元素)放到它该放的位置上
//即将划分值和大于划分值区域中,最接近划分值区域的元素交换。至此完成所有值的区域划分
swap(arr, more, r);
// 返回等于划分值的区域
return new int[] {less + 1, more};
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {3,1,2,4,5};
quickSort(arr);
for(int a: arr) {
System.out.print(a + " ");
}
}
}
六、堆排序
package codingTest3;
public class heapSort {
//利用堆结构完成排序
public static void heapSort(int[] arr) {
if(arr == null || arr.length < 2) {
return;
}
//每次先把当前数组变为一个大根堆,但是注意,这个大根堆并不一定是按顺序排列的;
//然后每次将这个大根堆的头结点与最后一个节点交换位置,接着将堆的大小减1
//1、首先先把这个数组变成大根堆
//2、然后让当前的最后一个元素与堆顶的元素进行交换。即通过hearify,将目前的堆顶的元素下沉。
// 最后再使堆的大小减1;(相当于最大的数的位置就固定住了)
//3、再从剩下的位置,重新调整为大根堆;再让当前最后一个元素,与栈顶元素进行交换,然后使堆的大小减1。
for(int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int size = arr.length;
swap(arr, 0, --size);//已经变成大根堆了
while(size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
}
//这个函数是建堆方法
//arr是数组(我们的堆可以用数组结构来表示,定义某种规则,可以在逻辑概念上产生与之相对应的完全二叉树),
//index是当前比较的数的下标,size用来控制整个堆的大小,(当整个完全二叉树的大小是整个数组的时候)
//循环的终止条件:当左孩子超过堆(即数组)大小的时候,就会终止循环
//首先,先将某节点的左节点还有右节点的值进行比较,将比较大的数,对应的下标值,存在largest变量中。
//然后,再用当前要比较的数,与刚才比较出较大的数值进行比较,看谁大,把较大那个数的下标赋值给largest。如果当前下标的值与下标largest对应的值相等,则退出这个循环。
//以上步骤,可以找到当前子树最大的值的下标,存在largest里面
//将当前对应的数,与下标为largest的数,进行数值交换。
//然后将当前的index,改变为largest;
//将left改为当前的index*2+1
public static void heapify(int[] arr, int index, int size) {
int left = 2 * index + 1;
//我觉得也可以写成:2 * index <= size
//也可以写成2 * index + 2 <= size
while(left < size) {
int largest = left + 1 < size && arr[left + 1] > arr[left]?left + 1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if(largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
//插入数值,设定插入的数组,以及其对应的下标;
//终止条件是:直到当前节点不比父节点大的时候,停止交换(当前节点比父节点大,就一直交换)
//用当前节点与其父节点进行比较,只要说当前节点比父节点的值大的话,就让该结点与其父节点进行交换;
//并且,改变当前下标为父节点下标。就是说该结点会往上跑
public static void heapInsert(int[] arr, int index) {
while(arr[index] > arr[(index-1)/2]) {
swap(arr, index, (index-1)/2);
index = (index - 1)/2;
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = {5,6,1,2,3,8};
heapSort(arr);
for(int num : arr) {
System.out.print(num + " ");
}
}
}
七、常见排序算法的稳定性
堆排序、快速排序、希尔排序、直接选择排序不是稳定的排序算法;
而基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。
首先,排序算法的稳定性大家应该都知道,通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,如果Ai = Aj, Ai原来在位置前,排序后Ai还是要在Aj位置前。
其次,说一下稳定性的好处。排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就 是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。
回到主题,现在分析一下常见的排序算法的稳定性,每个都给出简单的理由。
(1)冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改 变,所以冒泡排序是一种稳定排序算法。
(2)选择排序
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个 元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么 交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
(3)插入排序
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开 始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相 等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳 定的。
(4)快速排序
快速排序有两个方向,左边的i下标一直往右走,当a[i] <= a[center_index],其中center_index是中枢元素的数组下标,一般取为数组第0个元素。而右边的j下标一直往左走,当a[j] > a[center_index]。如果i和j都走不动了,i <= j, 交换a[i]和a[j],重复上面的过程,直到i>j。 交换a[j]和a[center_index],完成一趟快速排序。在中枢元素和a[j]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5 3 3 4 3 8 9 10 11, 现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱,所以快速排序是一个不稳定的排序算法,不稳定发生在中枢元素和a[j] 交换的时刻。
(5)归并排序
归并排序是把序列递归地分成短序列,递归出口是短序列只有1个元素(认为直接有序)或者2个序列(1次比较和交换),然后把各个有序的段序列合并成一个有 序的长序列,不断合并直到原序列全部排好序。可以发现,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也没有人故意交换,这不会破坏稳定 性。那么,在短的有序序列合并的过程中,稳定是否受到破坏?没有,合并过程中我们可以保证如果两个当前元素相等时,我们把处在前面的序列的元素保存在结 果序列的前面,这样就保证了稳定性。所以,归并排序也是稳定的排序算法。
(6)基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优 先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
(7)希尔排序(shell)
希尔排序是按照不同步长对元素进行插入排序,当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小, 插入排序对于有序的序列效率很高。所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元 素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
(8)堆排序
我们知道堆的结构是节点i的孩子为2*i和2*i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, ...1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没 有交换,那么这2个相同的元素之间的稳定性就被破坏了。所以,堆排序不是稳定的排序算法。
综上,得出结论: 选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。
二分查找
https://blog.youkuaiyun.com/volcano1995/article/details/88257708