数据结构排序算法

 

一、直接插入排序

(1)算法思想:假设第一个数是有序的,那么把后面的数拿出来插入到这个有序数的合适位置,假设是升序(比第一个数小则向后移动第一个数,将数插入到第一个数的前面),插入后有序区间扩大为两个,依次向后,不断拿出新的数插入到有序区间,再扩大这个有序区间直至区间大小等于排序数组的大小。

(2)时间复杂度:时间上,最好情况当序列已经是有序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可,复杂度O(n)。最坏情况,序列与目标序列相反,那么此时需要进行的比较共有n(n-1)/2次,时间复杂度忽略系数,结果为O(n^2)。平均来说插入排序算法复杂度为O(n²)。

(3)空间复杂度:由于插入排序没有进行任何开辟空间或者递归的操作,顾其空间复杂度为O(1).

  (4)适用场景:由于插入排序的时间复杂度太大,所以不适合大量数据的排序,如果数据量少,倒是没啥影响。适用于数据量小时使用。并且大部分已经被排序的场景。

 (5)代码实现:直接插入排序可以用两个循环完成:

  1. 第一层循环:遍历待比较的所有数组元素
  2. 第二层循环:将本轮选择的元素(selected)与已经排好序的元素(ordered)相比较。
    如果:selected > ordered,那么将二者交换
#直接插入排序
def insert_sort(L):
    #遍历数组中的所有元素,其中0号索引元素默认已排序,因此从1开始
    for x in range(1,len(L)):
    #将该元素与已排序好的前序数组依次比较,如果该元素小,则交换
    #range(x-1,-1,-1):从x-1倒序循环到0
        for i in range(x-1,-1,-1):
    #判断:如果符合条件则交换
            if L[i] > L[i+1]:
                temp = L[i+1]
                L[i+1] = L[i]
                L[i] = temp

void print(int a[], int n ,int i){  
    cout<<i <<":";  
    for(int j= 0; j<8; j++){  
        cout<<a[j] <<" ";  
    }  
    cout<<endl;  
}  
  
  
void InsertSort(int a[], int n)  
{  
    for(int i= 1; i<n; i++){  
        if(a[i] < a[i-1]){               //若第i个元素大于i-1元素,直接插入。小于的话,移动有序表后插入  
            int j= i-1;   
            int x = a[i];        //复制为哨兵,即存储待排序元素  
            a[i] = a[i-1];           //先后移一个元素  
            while(x < a[j]){  //查找在有序表的插入位置  
                a[j+1] = a[j];  
                j--;         //元素后移  
            }  
            a[j+1] = x;      //插入到正确位置  
        }  
        print(a,n,i);           //打印每趟排序的结果  
    }  
      
}  
  
int main(){  
    int a[8] = {3,1,5,7,2,4,9,6};  
    InsertSort(a,8);  
    print(a,8,8);  
}

二、折半插入排序

思想:折半插入排序是基于直接插入排序进行改写的,其可以减少"移动"和"比较"的次数

时间复杂度:O(n^2)

空间复杂度:O(1)

稳定性:稳定

/**
     * 折半插入排序
     * 优点:可以减少"比较"和"移动"的次数
     * @param arr
     * @return
     */
    public static int[] BInsertSort(int[] arr){
        for(int i=1;i<arr.length;i++)
        {
            //待插入元素
            int temp = arr[i];
            int j;
            int low = 0, high = i-1;
            while(low <= high)  //在arr[low..high]中折半查找有序插入的位置
            {
                int m = (low + high)/2;//折半
                if(temp < arr[m])
                {
                    high = m-1;  //插入点在低半区
                }
                else
                {
                    low = m+1;  //插入点在高半区
                }
            }
            
            //记录后移
            for(j=i-1;j>=high+1;j--)
            {
                arr[j+1] = arr[j];
            }
            arr[j+1] = temp;
        }
        return arr;
    }

三、希尔排序(缩小增量排序)

(1)算法思想:希尔排序可以认为是对直接插入排序的优化,我们知道,直接插入排序在基本有序时是非常快的,所以希尔排序就是在直接插入排序之前进行多趟预排序(直接插入排序每次只能将数据移动一个位置,希尔的优化就体现在一次可跳跃移动),使得排序数组接近有序,最后进行一趟直接插入排序。

将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。

预排序的思想如下:

(2)时间复杂度分析:希尔排序的时间复杂度介于O(n)至O(n^2)之间,相关资料显示其具体复杂度为O(n^1.3)次方,这里我没有深究。

(3)空间复杂度:与直接插入排序一样,O(1)。

(4)代码实现:希尔排序的总体实现应该由三个循环完成:

  1. 第一层循环:将gap依次折半,对序列进行分组,直到gap=1
  2. 第二、三层循环:也即直接插入排序所需要的两次循环。具体描述见上。
#希尔排序
def insert_shell(L):
    #初始化gap值,此处利用序列长度的一般为其赋值
    gap = (int)(len(L)/2)
    #第一层循环:依次改变gap值对列表进行分组
    while (gap >= 1):
    #下面:利用直接插入排序的思想对分组数据进行排序
    #range(gap,len(L)):从gap开始
        for x in range(gap,len(L)):
    #range(x-gap,-1,-gap):从x-gap开始与选定元素开始倒序比较,每个比较元素之间间隔gap
            for i in range(x-gap,-1,-gap):
    #如果该组当中两个元素满足交换条件,则进行交换
                if L[i] > L[i+gap]:
                    temp = L[i+gap]
                    L[i+gap] = L[i]
                    L[i] =temp
    #while循环条件折半
        gap = (int)(gap/2)

四、选择排序

(1)基本思想:在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止。

(2)时间复杂度:选择排序在最好和最坏的情况下都是O(n^2),因为,即使有序了,选择排序依然每次要进行固定的选择和比较。
(3)空间复杂度:O(1)

(4)代码实现:第一层循环:依次遍历序列当中的每一个元素
第二层循环:将遍历得到的当前元素依次与余下的元素进行比较,符合最小元素的条件,则交换。

# 简单选择排序
def select_sort(L):
#依次遍历序列中的每一个元素
    for x in range(0,len(L)):
#将当前位置的元素定义此轮循环当中的最小值
        minimum = L[x]
#将该元素与剩下的元素依次比较寻找最小元素
        for i in range(x+1,len(L)):
            if L[i] < minimum:
                temp = L[i];
                L[i] = minimum;
                minimum = temp
#将比较后得到的真正的最小值赋值给当前位置
        L[x] = minimum

一次选两个数的版本(二元选择排序):


#pragma once
#include<iostream>
 
using namespace std;
 
void SelectSort(int *a,size_t n)
{
	size_t max, min;
 
	size_t left = 0;
	size_t right = n - 1;
 
	while (left < right)
	{
		min = max = left;
		for (size_t j = left; j <= right; j++)
		{
			if (a[j] <= a[min])
				min = j;
			if (a[j]>=a[max])
				max = j;
		}
		swap(a[left], a[min]);
		if (left == max)
		{
			max = min;
		}
		swap(a[right], a[max]);
		left++;
		right--;
	}
}
 
void PrintArr(int* a,size_t n)
{
	for (size_t i = 0; i < n; i++)
	{
		cout << a[i] << " ";
	}
	cout << endl;
}
 
void TestSelectSort()
{
	int a[] = { 9, 5, 4, 2, 3, 6, 8, 7, 1, 0 };
	SelectSort(a, sizeof(a) / sizeof(a[0]));
	PrintArr(a, sizeof(a) / sizeof(a[0]));
}
void SelectSort(int r[],int n) {  
    int i ,j , min ,max, tmp;  
    for (i=1 ;i <= n/2;i++) {    
        // 做不超过n/2趟选择排序   
        min = i; max = i ; //分别记录最大和最小关键字记录位置  
        for (j= i+1; j<= n-i; j++) {  
            if (r[j] > r[max]) {   
                max = j ; continue ;   
            }    
            if (r[j]< r[min]) {   
                min = j ;   
            }     
      }    
      //该交换操作还可分情况讨论以提高效率  
      tmp = r[i-1]; r[i-1] = r[min]; r[min] = tmp;  
      tmp = r[n-i]; r[n-i] = r[max]; r[max] = tmp;   
  
    }   
}

五、堆排序

1、堆的概念:堆本质是一种数组对象。特别重要的一点性质:任意的叶子节点小于(或大于)它所有的父节点。对此,又分为大顶堆和小顶堆,大顶堆要求节点的元素都要大于其孩子,小顶堆要求节点元素都小于其左右孩子,两者对左右孩子的大小关系不做任何要求。
利用堆排序,就是基于大顶堆或者小顶堆的一种排序方法。下面,我们通过大顶堆来实现。

2、基本思想: (1)首先将序列构建称为大顶堆;(这样满足了大顶堆那条性质:位于根节点的元素一定是当前序列的最大值)。

 

(2)取出当前大顶堆的根节点,将其与序列末尾元素进行交换;(此时:序列末尾的元素为已排序的最大值;由于交换了元素,当前位于根节点的堆并不一定满足大顶堆的性质)

(3)对交换后的n-1个序列元素进行调整,使其满足大顶堆的性质;

(4)重复2.3步骤,直至堆中只有1个元素为止

3、时间复杂度:建堆的时间复杂度近似为O(n*log n),每次选一个数后进行调整的复杂度也近似为O(n*log n),时间复杂度忽略系数,结果就近似为O(n*log n).

4、空间复杂度:O(1)

5、代码实现:

六、冒泡排序

(1)基本思想:在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。

冒泡排序的示例:

 

(2)时间复杂度:第一趟排序需要经过(n-1)次比较,第二次(n-2),。。。等差数列,最后忽略系数还是O(n^2)。

(3)空间复杂度:O(1)

(4)代码实现:

#冒泡排序
def bubble_sort(L):
    length = len(L)
#序列长度为length,需要执行length-1轮交换
    for x in range(1,length):
#对于每一轮交换,都将序列当中的左右元素进行比较
#每轮交换当中,由于序列最后的元素一定是最大的,因此每轮循环到序列未排序的位置即可
        for i in range(0,length-x):
            if L[i] > L[i+1]:
                temp = L[i]
                L[i] = L[i+1]
                L[i+1] = temp

(5)优化方案:

——优化外层循环:若在某一趟排序中未发现气泡位置的交换,则说明待排序的无序区中所有气泡均满足轻者在上,重者在下的原则,因此,冒泡排序过程可在此趟排序后终止。为此,在下面给出的算法中,引入一个标签flag,在每趟排序开始前,先将其置为0。若排序过程中发生了交换,则将其置为1。各趟排序结束时检查flag,若未曾发生过交换则终止算法,不再进行下一趟排序。


#include<stdio.h>
#include<stdlib.h>
 
int main()
{
	int arr[] = {6,5,4,3,2,1};
	int i = 0;
	int len  = sizeof(arr)/sizeof(arr[0]);
 
	BubbleSort3(arr,len);
 
	for(i = 0;i < len; i++)
		printf("%d ",arr[i]);
 
	system("pause");
	return 0;
}

//冒泡排序优化1
void BubbleSort2(int* arr, size_t size)
{
	assert(arr);
	int i = 0, j = 0;
 
	for (i = 0; i < size - 1; i++)//一共要排序size-1次
	{
		//每次遍历标志位都要先置为0,才能判断后面的元素是否发生了交换
		int flag = 0;
 
		for (j = 0; j < size - 1 - i; j++)//选出该趟排序的最大值往后移动
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag = 1;//只要有发生了交换,flag就置为1
			}
		}
 
		//判断标志位是否为0,如果为0,说明后面的元素已经有序,就直接return
		if (flag == 0)
		{
			return;
		}
	}
 
}

——优化内层循环:在每趟扫描中,记住最后一次交换发生的位置lastExchange,(该位置之后的相邻记录均已有序)。下一趟排序开始时,R[1..lastExchange-1]是无序区,R[lastExchange..n]是有序区。这样,一趟排序可能使当前无序区扩充多个记录,因此记住最后一次交换发生的位置lastExchange,从而减少排序的趟数。

//冒泡排序优化2
void BubbleSort3(int* arr, size_t size)
{
	assert(arr);
	int i = 0, j = 0;
	int k = size - 1,pos = 0;//pos变量用来标记循环里最后一次交换的位置  
	
	for (i = 0; i < size - 1; i++)//一共要排序size-1次
	{
		//每次遍历标志位都要先置为0,才能判断后面的元素是否发生了交换
		int flag = 0;
 
		for (j = 0; j <k; j++)//选出该趟排序的最大值往后移动
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
				flag = 1;//只要有发生了交换,flag就置为1
				pos = j;//循环里最后一次交换的位置 j赋给pos
			}
		}
 
		k = pos;
		//判断标志位是否为0,如果为0,说明后面的元素已经有序,就直接return
		if (flag == 0)
		{
			return;
		}
	}
 
}

七、快速排序

1、基本思想:

1)选择一个基准元素,通常选择第一个元素或者最后一个元素,

2)通过一趟排序讲待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大。

3)此时基准元素在其排好序后的正确位置

4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。

快速排序的示例:

(a)一趟排序的过程:

(b)排序的全过程

2、时间复杂度:O(nlogn),最坏的情况下为O(n^2)

(3)空间复杂度: 最优的情况下空间复杂度为:O(logn)  ;每一次都平分数组的情况

                                最差的情况下空间复杂度为:O( n )      ;退化为冒泡排序的情况

(4)代码实现:

#快速排序
#L:待排序的序列;start排序的开始index,end序列末尾的index
#对于长度为length的序列:start = 0;end = length-1
def quick_sort(L,start,end):
    if start < end:
        i , j , pivot = start , end , L[start]
        while i < j:
#从右开始向左寻找第一个小于pivot的值
            while (i < j) and (L[j] >= pivot):
                j = j-1
#将小于pivot的值移到左边
            if (i < j):
                L[i] = L[j]
                i = i+1 
#从左开始向右寻找第一个大于pivot的值
            while (i < j) and (L[i] < pivot):
                i = i+1
#将大于pivot的值移到右边
            if (i < j):
                L[j] = L[i]
                j = j-1
#循环结束后,说明 i=j,此时左边的值全都小于pivot,右边的值全都大于pivot
#pivot的位置移动正确,那么此时只需对左右两侧的序列调用此函数进一步排序即可
#递归调用函数:依次对左侧序列:从0 ~ i-1//右侧序列:从i+1 ~ end
        L[i] = pivot
#左侧序列继续排序
        quick_sort(L,start,i-1)
#右侧序列继续排序
        quick_sort(L,i+1,end)

(5)优化方案:https://blog.csdn.net/hacker00011000/article/details/52176100

八、 归并排序(Merge Sort)

1、基本思想:归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

归并排序示例:

 

(1)如何合并?
L[first...mid]为第一段,L[mid+1...last]为第二段,并且两端已经有序,现在我们要将两端合成达到L[first...last]并且也有序。

首先依次从第一段与第二段中取出元素比较,将较小的元素赋值给temp[]

重复执行上一步,当某一段赋值结束,则将另一段剩下的元素赋值给temp[]

此时将temp[]中的元素复制给L[],则得到的L[first...last]有序

(2)如何分解?
在这里,我们采用递归的方法,首先将待排序列分成A,B两组;然后重复对A、B序列
分组;直到分组后组内只有一个元素,此时我们认为组内所有元素有序,则分组结束。

2、时间复杂度:O(nlogn)

3、空间复杂度:O(n)

4、代码实现:

# 归并排序
#这是合并的函数
# 将序列L[first...mid]与序列L[mid+1...last]进行合并
def mergearray(L,first,mid,last,temp):
#对i,j,k分别进行赋值
    i,j,k = first,mid+1,0
#当左右两边都有数时进行比较,取较小的数
    while (i <= mid) and (j <= last):
        if L[i] <= L[j]:
            temp[k] = L[i]
            i = i+1
            k = k+1
        else:
            temp[k] = L[j]
            j = j+1
            k = k+1
#如果左边序列还有数
    while (i <= mid):
        temp[k] = L[i]
        i = i+1
        k = k+1
#如果右边序列还有数
    while (j <= last):
        temp[k] = L[j]
        j = j+1
        k = k+1
#将temp当中该段有序元素赋值给L待排序列使之部分有序
    for x in range(0,k):
        L[first+x] = temp[x]
# 这是分组的函数
def merge_sort(L,first,last,temp):
    if first < last:
        mid = (int)((first + last) / 2)
#使左边序列有序
        merge_sort(L,first,mid,temp)
#使右边序列有序
        merge_sort(L,mid+1,last,temp)
#将两个有序序列合并
        mergearray(L,first,mid,last,temp)
# 归并排序的函数
def merge_sort_array(L):
#声明一个长度为len(L)的空列表
    temp = len(L)*[None]
#调用归并排序
    merge_sort(L,0,len(L)-1,temp)


 

几大排序应用场景:

(1)若n较小(如n≤50),可采用直接插入或直接选择排序
     当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。
(2)若文件初始状态基本有序(指正序),则应选用直接插人、冒泡或随机的快速排序为宜;
(3)若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序
     快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
     堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
     若要求排序稳定,则可选用归并排序。但本章介绍的从单个记录起进行两两归并的  排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定 的,所以改进后的归并排序仍是稳定的。

https://blog.csdn.net/pointer_y/article/details/53354762

https://www.jianshu.com/p/7d037c332a9d

https://www.cnblogs.com/zlcxbb/p/5816725.html

https://www.cnblogs.com/xuzhp/p/4612303.html

https://blog.csdn.net/htq__/article/details/50984457

猜你喜欢

转载自blog.csdn.net/u012114090/article/details/81546386