持续更新中。。。。。。
根据比较与否,可将排序算法大致分为比较排序和非比较排序两大类:
稳定性问题定义:
如果arr[i] = arr[j],排序前arr[i]在arr[j]之前,排序后arr[i]还在arr[j]之前,则称这种排序算法是稳定的。通俗地讲就是保证排序前后两个相等的数的相对顺序不变。
对于以上比较类的排序算法而言,稳定性问题是可以在定义交换函数时打破这种稳定性的。
稳定性排序算法包括:
比较类的排序算法中,就复杂度最优情况而言:
原理很简单,类似于水中的一个气泡,从水底往上冒。冒泡排序就是需要进行n-1轮循环比较,每轮比较从0~n-1-i(i为次数编号)检查序列中的数,两两相邻进行比较,以升序排序为例就是:大的数往后放,这样进行完一次排序,大的数都是放在最后,直到所有次数排完也就是顺序的了。
最坏情况下需比较的次数:n(n-1)/2。
使用场景:n较小的情况。
Java代码实现:
public static void bubbleSort2(int[] arr){
for(int end = arr.length-1; end > 0; end--){
int border = 0;
for(int i = 0; i < end; i++){
if(arr[i] > arr[i+1]){
swap(arr, i, i+1); #交换函数,此处省略
border = i+1; #优化:记录此次结束位置,下次从该处进行比较
}
}
end = border;
}
}
Python代码实现:
def bubbleSort(arr):
n = len(arr)
#进行n-1躺排序
for i in range(1,n):
is_ordered = True # 是否有序序列标志
#比较次数
for j in range(n-i):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
is_ordered = False
if is_ordered:
return arr
在基本冒泡排序的基础上改进,采用两边同时冒泡的方法,也就是从左到右每次比较将大的数往后移的同时,从右到左将每次比较的最小数往前移,效率高于基本冒泡排序法。
Java代码实现:
/**把最大的数往后面冒泡的同时,最小的数也往前面冒泡**/
public static void cocktailSort(int[] arr) {
int L = 0,R = arr.length-1;
while(L < R) {
for(int i = L; i < R; i++) if(arr[i] > arr[i+1]) swap(arr,i,i+1);
R--;
for(int i = R; i > L; i--) if(arr[i] < arr[i-1]) swap(arr,i,i-1);
L++;
}
}
Python代码实现:
#鸡尾酒冒泡排序(改进版冒泡排序)
def cocktaiSort(arr):
left = 0
right = len(arr) - 1
while left < right:
#大的数往后排
for i in range(left, right):
if arr[i] > arr[i+1]:
arr[i], arr[i+1] = arr[i+1], arr[i]
right -= 1
for j in range(right, left, -1):
if arr[j] < arr[j-1]:
arr[j], arr[j-1] = arr[j-1], arr[j]
left += 1
return arr
选择排序顾名思义重在选择,那应该如何选择,每次比较选择怎样的数呢?
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
不稳定性主要表现在:
比如序列5 8 5 2 9,第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
最优时间复杂度:
即使已经是有序,还是得拿着前面的元素和后面的一个一个地进行比较,所以复杂度是O(n2)
最坏时间复杂度:
内层循环是和n有关的,复杂度是O(n2)
适用情况:n较小
public static void selectSort(int[] arr){
len = arr.length
for(int i = 0; i < len - 1 ; i++) {
//记录最小值的下标
int minIndex = i;
for (int j = i + 1; j < len; j++)
//从i+1开始遍历寻找最小值的索引
minIndex = arr[j] < arr[minIndex] ? j : minIndex;
swap(arr,i,minIndex); //交换函数
}
}
Python代码实现:
def select_sort(arr):
length = len(arr)
for i in range(length-1):
#记录最小值下标
min_index = i #初始化为第一个数
#从索引i+1后的序列开始遍历寻找最小值
for j in range(i+1, length):
if arr[min_index] > arr[j]:
min_index = j
#若已是最小值,就不交换
if min_index != i:
arr[i], arr[min_index] = arr[min_index], arr[i]
return arr
插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
可优化思路:
因为前一部分已经是从小到大排列的了, 所以如果从后面选出的最小元素大于前面的元素,那么就一定比前面的前面还要大,这时候就不需要进行比较了。
最优算法复杂度:
假设列表已经是从小到大排序好, 那么while循环进入一次就退出,总共进入n-1次,所以算法复杂度是O(n)。
最坏算法复杂度:
假设列表是完全无序,或者说是从大到小排列的,那么内层循环是n-1 n-2 n-3 …,和n是有关的,所以复杂度是O(n2)。
稳定性:
拿无序的第一个元素和前面的比较,比如说前面最大66,后面有一个66,因为后面的66不比前面的66大,位置不改变,所以插入排序是稳定的。
使用场景:数据量小时使用。并且大部分已经被排序。
Java代码实现:
public static void insertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int key = arr[i], j;
for (j = i - 1; j >= 0 && key < arr[j]; j--) arr[j + 1] = arr[j]; //中间的元素往后面移动
arr[j + 1] = key; //将key插入到合适的位置
}
}
Python代码实现:
def insert_sort(arr):
length = len(arr)
for i in range(1, length): #默认第一个数已排序,从索引1开始记作未排序
for j in range(i, 0, -1): #从后往前遍历已排序数据并比较
if arr[j] < arr[j-1]:
arr[j], arr[j-1] = arr[j-1], arr[j] #交换位置即插入
else: #优化,如果未排序的第一个数大于等于已排序的最后一个数,则无需比较交换
break
print(arr)
return arr
if __name__ == '__main__':
arr = list(map(int, input().split(" ")))
print(insert_sort(arr))
二分插入排序是采用二分搜索法对插入排序进行改进。改进思路:在前面已经排好序的序列中找当前要插入的元素的位置时,采用二分查找的方式去找那个插入的位置(也就是大于key的那个位置) ,找到那个位置之后,再进行元素的移动,最后把那个元素插入到找到的那个位置。
Java代码实现:
public static void insertSort(int[] arr){
int n = arr.length;
for(int i=1; i<n; i++){
int key = arr[i];
//二分查找已排序序列
int left = 0;
int right = i -1;
while(left <= right){
int mid = left + (right - left)/2;
if(arr[mid] > key){ //说明在左半部分,则缩小右边界
right = mid -1;
}else{ //否则在右半部分,缩小左边界
left = mid + 1;
}
}
//二分查找完后,只需要从左边界遍历即可,也就是刚好大于key的那个位置
for(int j=i-1; j>=left; j--) arr[j+1] = arr[j];
arr[j+1] = key;
}
}
Python代码:
def insert_sort_impl(arr):
n = len(arr)
for i in range(1, n):
key = arr[i]
l, r = 0, i-1
while l <= r:
mid = l + (r - l) // 2 # mid = (l + r) // 2
if arr[mid] > key:
r = mid -1
else:
l = mid + 1
#只需移动l即之后的位置
for j in range(i-1, l-1, -1):
arr[j+1] = arr[j]
arr[l] = key #原左边界的位置就是要插入的元素的位置
return arr
if __name__ == '__main__':
arr = list(map(int, input().split(" ")))
print(insert_sort_impl(arr))
希尔排序是插入排序的进阶版,算法思路:
(1)按步长把原来的序列分为好几部分,每一个部分采用插入排序;
(2)调整步长,重复这个过程
希尔排序相对于插入排序的优势在于插入排序每次只能将数据移动一位,不过希尔排序时间复杂度的大小还是要取决于步长的合适度,另外希尔排序不是一种稳定的排序算法。
图解:
Java代码实现:
public static void shellSort2(int[] arr) {
for(int gap = arr.length; gap > 0; gap /= 2) { //增量序列
for(int i = gap; i < arr.length; i++) { //从数组第gap个元素开始
int key = arr[i],j; //每个元素与自己组内的数据进行直接插入排序
for(j = i-gap; j >= 0 && key < arr[j]; j -= gap) arr[j+gap] = arr[j];
arr[j+gap] = key;
}
}
}
Python代码实现:
def shell_sort(arr):
length = len(arr)
gap = length // 2
while gap > 0: #步长必须能取到1
for i in range(gap,length): #插入排序写法,不同的地方是加入了步长
for j in range(i, 0, -gap):
if arr[j] < arr[j - gap]:
arr[j], arr[j-gap] = arr[j-gap], arr[j]
else:
break
gap //= 2
return arr
if __name__ == '__main__':
arr = list(map(int, input().split(" ")))
print(shell_sort(arr))
快速排序的思想其实很简单:首先找一个基准数,一般方便直接以数组第一个数作为基准数,然后初始化两个最左和最右指针,先移动最右的指针,找到比基准数小的数就停止,然后移动最左边的指针找到比基准数大的数停止,此时交换两个指针指向的数,然后继续右指针先移动重复该过程,知道两个指针相遇,此时交换基准数和两个指针共同指向的那个数,这样就完成了第一轮的划为,以基准数为划分点,做左边的都是小于基准数的,最右边的都是大于基准数的。后续再用递归快排下左右两个部分就好了。
图解可以参考一下这篇博客:https://blog.csdn.net/shujuelin/article/details/82423852
使用场景:是最快的通用排序算法,大多数使用情况下,是最佳选择。
Java代码实现:
public class QuickSort {
public static void quickSort(int[] arr, int low, int height){
int i, j, temp, t;
if(low > height){
return;
}
i = low; //左指针
j = height; //右指针
temp = arr[i]; //基准数
while(i < j){
while(temp<=arr[j] && i<j) j--; //右指针左移
while(temp>=arr[i] && i<j) i++; //左指针右移
if(i < j){ //交换两个指针指向的数
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
//交换基准数与两指针相遇时指向的数
arr[low] = arr[i];
arr[i] = temp;
//递归
quickSort(arr, low, j-1);
quickSort(arr, j+1, height);
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String str = sc.nextLine();
String[] strs = str.split(" ");
int[] arr = new int[strs.length];
for(int i=0; i<strs.length; i++){
arr[i] = Integer.parseInt(strs[i]);
}
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
}
归并排序思路:将一个待排序的数组不断分成左右两部分进行递归排序,最后合并已排好序的即可。
归并排序也是分治法一个很好的应用,先递归到最底层,然后从下往上每次两个序列进行归并合起来,是一个由上往下分开,再由下往上合并的过程,而对于每一次合并操作,过程如下:
(1)申请一个额外的辅助数组空间,长度与原数组长度一样,用来存放合并好的序列;
(2)设置两个指针,起始位置分别为左右排好序的起始位置;
(3)比较两个指针指向的数,并将较小的数存放到合并数组中,然后继续移动指针,指针指向末尾结束;
(4)将剩余的数直接复制到合并数组的末尾。
使用场景:如果需要稳定,空间不是很重要,就选择归并排序。
Java代码实现:
public class 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) {
if (l == r) {
return;
}
int mid = l + ((r - l) >> 1); //mid = (L+R)/2,取中点,但这种写法容易溢出
//所以文中写法相当于mid=L+(R-L)/2,然后位运算效率更快
mergeSort(arr, l, mid);//T(n/2)
mergeSort(arr, mid + 1, r);//T(n/2)
merge(arr, l, mid, r);//O(N)
//T(N)=2T(N/2)+O(N)
}
//合并
public static void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
while (p1 <= m && p2 <= r) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
//两个必有一个越界
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
}
(1)堆的基本性质:
堆排序的适用场景有两个:
(3)Java代码实现:
public class HeapSort {
public static void sort(int[] arr){
//1、构建初始大顶堆,从第一非叶子节点开始
for(int i=arr.length/2 - 1; i>=0; i--){
adjustHeap(arr, i, arr.length);
}
//2、调整继续调整堆结构并交换堆顶元素
for(int j=arr.length-1; j>0; j--){
//交换函数
swap(arr, 0, j);
//重新构建堆,重复步骤1
adjustHeap(arr, 0, j);
}
}
private static void adjustHeap(int[] arr, int i, int length){
//保存当前结点
int temp = arr[i];
//从i结点的左子节点开始,也就是2*i+1
for(int k=i*2+1; k<length; k=k*2+1){
//如果左子节点小于右子节点,k指向右子节点
if(k+1<length && arr[k]<arr[k+1]){
k++;
}
//如果子节点大于父节点,则和父节点进行交换
if(arr[k] > temp){
arr[i] = arr[k]; //将大的结点作为父节点
i = k;
}else{
break;
}
}
arr[i] = temp; // 将temp值放到尾部
}
private 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) {
Scanner sc = new Scanner(System.in);
//获取输入的字符串序列
String str = sc.nextLine(); //输入一行字符串
String[] strs = str.split(" "); //空格进行切割并保存为字符串数组
//转换为整数数组
int[] arr = new int[strs.length];
for(int i=0; i<strs.length; i++){
arr[i] = Integer.parseInt(strs[i]);
}
sort(arr);
System.out.println(Arrays.toString(arr));
}
}
总结:
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。
待更新。。。。。。
待更新。。。。。。
待更新。。。。。。
参考资料:
[1]https://blog.csdn.net/zxzxzx0119/article/details/79826380#t13
[2]https://www.cnblogs.com/ladder/p/10685051.html
[3]https://blog.csdn.net/u014736619/article/details/80549698
[4]https://blog.csdn.net/shujuelin/article/details/82423852