算法: 描述一种有限、确定、有效的并合适计算机程序来实现用于解决问题的方法
一般与数据结构组合起来使用,算法提供方法思想,数据结构提供数据的组织方式
稳定性:排序中相等的元素保留之前的相对顺序我们就是算法是稳定的。
排序算法有很多,包括插入排序,冒泡排序,堆排序,归并排序,选择排序,计数排序,基数排序,桶排序,快速排序等。插入排序,堆排序,选择排序,归并排序和快速排序,冒泡排序都是比较排序,它们通过对数组中的元素进行比较来实现排序,其他排序算法则是利用非比较的其他方法来获得有关输入数组的排序信息。
1.冒泡排序
分为n-1趟,每趟确定一个数的最终排序位置,为每次排序范围的最后一个位置
从第一个元素开始,依次比较相邻元素的大小,将大的元素往后传。
冒泡是稳定的,因为两两相等的元素是不会交换的,若两个相等的元素没有相邻,那么通过前面的两两交换把两个元素相邻起来,他们也不会交换。
冒泡的比较次数是固定的,1+2+...+(n-1) = n*(n-1)/2
Java描述:
public static void bubbleSort(int[] arr){
for(int i =1; i < arr.length-i; i++){
for(int j =0; j<arr.length-i; j++) {
if (arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
2.选择排序
也是分为n-1趟,每趟也确定一个最终排序位置,为每次排序范围的第一个位置
从第一个位置开始,依次往后比较相应位置上的元素大小,遇到更小的元素就与之交换。
选择是不稳定的,举例:在一趟选择中,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就受到破坏了。
选择的比较次数也是固定的,1+2+...+(n-1) = n*(n-1)/2
交换次数O(n),最好情况是,已经有序,交换0次;最坏情况交换n-1次,逆序交换n/2次。交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。
Java描述:
public static void selectionSort(int[] arr){
for(int i = 0; i < arr.length - 1; j++){
int min = i;
for(int j = i + 1; j < arr.length; j++){
if (arr[min] > arr[j]){
min = j;
}
}
if (min != j){
int tmp = arr[min];
arr[min] = arr[i];
arr[i] = arr[min];
}
}
}
3.插入排序
插入排序是一个简单直观且稳定的排序算法。
也是分为n-1趟,每趟插入一个元素到已有的有序队列中,从而得到一个新的个数加一的有序队列,适用于少量的数据的排序。
插入算法把要排序的数组分成两部分:第一部分包含了这个数组的所有元素,但将最后一个元素除外(让数组多一个空间才有插入的位置),而第二部分就只包含这一个元素(即待插入元素)。在第一部分排序完成后,再将这个最后元素插入到已排好序的第一部分中。
稳定的原因:
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
Java描述:
public static int[] insertSort(int[] arr){
if (arr == null || arr.length < 2){
return arr;4.
}
for (int i = 1; i < arr.length; i++){
int temp = arr[i];
int index = i;
for (int j = i; j > 0; j--){
if (arr[j] < arr[j-1]){
arr[j] = arr[j-1];
index = j - 1;
}else{
break;
}
arr[index] = temp;
}
}
return arr;
}
4.希尔排序
希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
-
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
-
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
希尔排序的时间性能优于直接插入排序的原因:
①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
②当n值较小时,n和 n^2 的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n^2)差别不大。
③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
因此,希尔排序在效率上较直接插入排序有较大的改进。
Java描述:
public static void ShellSort(int[] arr){
int len = arr.length;
while (len != 1){
len /= 2;
for (int i = 0; i < len; i++){
for (int j = i + len; j < arr.length; j += len){
int temp = arr[j];
for (k = j - len; k >= 0 && arr[k] >temp; k -= len){
arr[k+len] = arr[k];
}
arr[k + len] = temp;
}
}
}
}
5.归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并操作的工作原理如下:
第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾
将另一序列剩下的所有元素直接复制到合并序列尾
归并排序是稳定的排序.即相等的元素的顺序不会改变.如输入记录 1(1) 3(2) 2(3) 2(4) 5(5) (括号中是记录的关键字)时输出的 1(1) 2(3) 2(4) 3(2) 5(5) 中的2 和 2 是按输入的顺序.这对要排序数据包含多个信息而要按其中的某一个信息排序,要求其它信息尽量按输入的顺序排列时很重要。归并排序的比较次数小于快速排序的比较次数,移动次数一般多于快速排序的移动次数。
速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。
Java描述:
public static int[] mergeSort(int[] nums, int l , int h){
if (l == h){
return new int[] {nums[l]};
}
int mid = l + (h - l) / 2;
int[] leftArr = mergeSort(nums, l, mid); // 左有序数组
int[] rightArr = mergeSort(nums, mid + 1, h); // 右有序数组
int[] newArr = new int[h-l+1]; // 新有序数组
int m = 0, i = 0, j = 0;
while (i < leftArr.length && j < rightArr.length){
newArr[m++] = leftArr[i] < rightArr[j] ? leftArr[i++] : right[j++];
}
while (i < leftArr.length){
newArr[m++] = leftArr[i++];
}
while (j < rightArr.length){
newArr[m++] = rightArr[j++];
}
return newArr;
}
6.快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。
它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
(1)首先设定一个分界值,通过该分界值将数组分为左右两个部分。
(2)将大于或者等于分界值的数据集中到数组右边(所以是不稳定的),小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或者等于分界值,而右边部分中的元素都大于或者等于分界值。
(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以去一个分界值,将该部分分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分拍好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
1)设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j--),找到第一个小于key的值A[j],将A[j]和A[i]的值交换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换;
5)重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
快速排序的一次划分算法从两头交替搜索,直到low和high重合,因此其时间复杂度是O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为O(nlog2n)。
最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n2)。
为改善最坏情况下的时间性能,可采用其他方法选取中间数。通常采用“三者值取中”方法,即比较H->r[low].key、H->r[high].key与H->r[(1ow+high)/2].key,取三者中关键字为中值的元素为中间数。
可以证明,快速排序的平均时间复杂度也是O(nlog2n)。因此,该排序方法被认为是目前最好的一种内部排序方法。
从空间性能上看,尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归。最好的情况下,即快速排序的每一趟排序都将元素序列均匀地分割成长度相近的两个子表,所需栈的最大深度为log2(n+1);但最坏的情况下,栈的最大深度为n。这样,快速排序的空间复杂度为O(log2n))。
Java描述:
//方式一********************
public static int[] qsort(int arr[], int start, int end){
int pivot = arr[start];
int i = start;
int j = end;
while (i<j){
while ((i<j) && (arr[j] > pivot)){
j--;
}
while ((i<j) && arr[i] < pivot){
i++;
}
if ((arr[i] == arr[j]) && (i<j)){
i++;
}else{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
if (i - 1 > start){
arr = qsort(arr, start, i - 1);
}
if (j + 1 < end){
arr = qsort(arr, j + 1; end);
}
return arr;
}
//方式二(更高效)***********************TextendsComparable和SortUtil都是自己封装的类,里面重写和实现了compareTo
//和swap方法
public <TextendsComparable<? super T>> T[] quickSort(T[] targetArr, int start, int end){
int i = start + 1, j = end;
T key = targetArr[start];
SortUtil<T> sUtil = new SortUtil<>();
if (start == end){
return targetArr;
}
while (true){
while (targetArr[j].compareTo(key) > 0){
j--;
}
while (targetArr[i].compareTo(key) < 0 && i < j){
i++;
}
if (i >= j){
break;
}
sUtil.swap(targetArr, i, j);
if (targetArr[i] == key){
j--;
}else{
i++;
}
}
sUtil.swap(targetArr, start, j); // 将关键数据放到中间来
if (start < i - 1){
this.quickSort(targetArr, start, i - 1);
}
if (j + 1 < end){
this.quickSort(targetArr, j + 1, end);
}
return targetArr;
}
//方法三*********************减少交换次数,提高效率
public <TextendsComparable<? super T>> void quickSort(T[] targetArr, int start, int end){
int i =start, j = end;
T key = targetArr[start];
while (i < j){
/*按j--方向遍历目标数组,直到比key小的值为止*/
while (j > i && targetArr[j].compareTo(key) > 0){
j--;
}
if (i < j){
/*targetArr[i]已经保存在key中,可将后面的数填入*/
targetArr[i] = targetArr[j];
i++;
}
/*按i++方向遍历目标数组,直到比key大的值为止*/
while (i < j && targetArr[i].compareTo(key) <= 0){
/*此处一定要小于等于零,假设数组之内有一亿个1,0交替出现的话,而key的值又恰巧是1的话,那么这个小于等于的作用就会使下面的if语句少执行一亿次。*/
i++;
}
if (i < j){
/*targetArr[j]已保存在targetArr[i]中,可将前面的值填入*/
targetArr[j] = targetArr[i];
j--;
}
}
targetArr[i] = key;
if (start < i - 1){
this.quickSort(targetArr, start, i - 1);
}
if (j + 1 < end){
this.quickSort(targetArr, j + 1, end);
}
}
7.堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
-
最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
-
创建最大堆(Build Max Heap):将堆中的所有数据重新排序
-
堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
Java描述:
public static int[] heapSort(int[] arr){
//这里元素的索引是从0开始的,所以最后一个非叶子结点array.length/2 - 1
for (int i = arr.length/2 - 1; i >= 0; i--){
adjustHeap(arr, i, arr.length); // 调整堆
}
// 上述逻辑,建堆结束
// 下面开始排序逻辑
for (int j = arr.length - 1; j > 0; j--){
// 元素交换,作用是去掉大顶堆
// 把大顶堆的根元素,放到数组的最后;换句话说,就是每一次的堆调整之后,都会有一个元素到达自己的最终位置
swap(arr, 0, j);
// 元素交换之后,毫无疑问,最后一个元素无需再考虑排序问题了。
// 接下来我们需要排序的,就是已经去掉了部分元素的堆了,这也是为什么此方法放在循环里的原因
// 而这里,实质上是自上而下,自左向右进行调整的
adjustHeap(arr, 0, j);
}
return arr;
}
public static void adjustHeap(int[] arr, int i, int length){
// 先把当前元素取出来,因为当前元素可能要一直移动
int temp = arr[i];
for (int k = 2*i + 1; k < length; k = 2*k + 1){ //2*i+1为左子树i的左子树(因为i是从0开始的),2*k+1为k的左子树
// 让k先指向子节点中最大的节点
if (k + 1 < length && arr[k] < arr[k+1]){ // 如果有右子树,并且右子树大于左子树
k++;
}
//如果发现结点(左右子结点)大于根结点,则进行值的交换
if (arr[k] > temp){
swap(arr, i, k);
// 如果子节点更换了,那么,以子节点为根的子树会受到影响,所以循环对子节点所在的数继续进行判断
}else{
break; // 不用交换,退出循环
}
}
}