八大排序算法原理及复杂度分析----用C实现

排序算法

选择排序

直接选择排序

直接选择排序思想是先对数组进行遍历选出一个最小的放到第一个位置,然后再对第一位以后的进行遍历选择最小的放到第二位… 反复操作后就会得到一个从小到大的数组。

对这种思想进行优化,每次遍历选出最小和最大的数据,最小的放到第一个位置,最大的放到最后一个位置,然后缩小范围,就可以得到一个有序序列。

最好的情况是数据本来就有序,复杂度为O(n);最差的情况是O(n^2),是不稳定算法。

代码如下:

void SelectSort(int* a, int n)
{
    int left = 0;
    int right = n - 1;
    while(left < right)
    {
        int mini = left, maxi = right;
        for (int i = left + 1; i <= right; i-- )
        {
            if (a[i] < a[mini])
            {
                mini = i;
            }
            
            if (a[i] > a[maxi])
            {
                maxi = i;
            }
        }
        Swap(&a[mini],&a[left]);
        if (mini == left)
            maxi = mini;
        Swap(&a[maxi],&a[right]);
        right--;
        left++;
    }
}

复杂度为O(n);最差的情况是O(n^2),是不稳定算法

堆排序

由上面堆的性质我们可以知道堆每个根一定是他的子树中最小或最大的结点,所以我们可以运用这个性质进行一个排序操作。

先对数组进行建堆,建一个大堆,然后将堆的根节点与最后一个结点进行交换,最后对新的堆进行向下调整。

void AdjustDown(DataType* a, size_t size, size_t root) 
{
	size_t parent = root;
	size_t child = parent * 2 + 1;
	while (child < size) 
    {
		if (child + 1 < size && a[child + 1] < a[child]) 
        {
			child++;
		}

		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n) 
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--) 
    {
		AdjustDown(a, n, i);
	}//向下调整建立大堆

	size_t end = n - 1;
	while (end > 0) 
    {
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

算法复杂度分析:建堆的算法复杂度为O(N),每次交换然后再进行向下调整的算法复杂度为O(NlogN),所以堆排序的算法复杂度为O(NlogN),由于直接在数组上进行操作空间复杂度为O(1)。

交换排序

冒泡排序

冒泡排序是在数组中从头开始,两两比较大小,将大的数据放在后一位,然后一趟遍历就可以找到最大的数据,然后减小排序区间进行下一次遍历。

代码如下:

void BubbleSort(int* a, int n) 
{
    for (int i = 0; i < n; i++) 
    {
        int flag = 0;//加入信号量,如果已经有序的情况下可以减少遍历
        for (int j = 0; j < n - i - 1; j++) 
        {
            if (a[j] > a[j+1]) 
            {
                flag = 1;
                swap(&a[j],&a[j+1]);
            }
        }
        if (flag = 0) 
        {
                break;
        }
     }  
}

算法复杂度为O(N^2)

快速排序

“hoare” 快速排序思想

每次找到一个target位置,一般将target设到第一个位置,然后用left和right两个指针进行搜索,right找比target值小的数值,left找比target值大的数值,然后两者交换。当left和right相交时将target交换到此位置,这样可以保证target的左边都比其值小,右边都比其值大。

由于right找的是比target值小的数值,所以每次让right先走,第一种情况是right找到了比其小的,然后left没找到位置就和right相遇了,此时right位置处的数还没有交换所以还是小于target的数。

第二种情况,right没找到,一直走到left,此时由于left和right上一次已经交换了数字,此时left处的数也是小于target的数。

这样就可以保证最后left和right相交的地方一定是小于target的。

如果target在最右边,就让left先走。

然后对target左边和target右边两个区间进行递归调用,直到无法再进行排序。

6-8-4-3-2-9-7这组数据为例,如图所示

在这里插入图片描述

代码如下:

int QuickSortPart(int* a, int left, int right)
{
    int target = left;
    while (left < right)
    {
        if (left < right && a[right] >= a[target])
        {
            right--;
        }
        
        if (left < right && a[left] <= a[target])
        {
            left++
        }
        
        Swap(&a[left],&a[right]);
    }
    Swap(&a[left],&a[target]);
    
    return left;
}

void QuickSort(int*a ,int begin, int end)
{
    if (begin >= end)
    {
        return;
    }
    
    int target = QuickSortPart(a, begin, end);
    QuickSort(a, begin, target - 1);
    QuickSort(a, target + 1, end);
}

挖坑法

挖坑法是在"hoare"的基础上进行了稍微的改变,首先,确定一个key位置,这里我们设key的位置在第一个位置,然后将key位置的数据进行保存,此时key位置就相当于一个坑位,然后两个双指针left和right,left指向头,right指向尾,right找小于key值的数值,找到后就将改数值“填入”刚才的“坑”中,此时坑位到了right现在的位置,然后left向右寻找大于key值的数据,找到后将数值填入“坑”中,重复进行上述操作,知道left和right走到同一个位置。

继续用 6-8-4-3-2-9-7这组数据为例

在这里插入图片描述

代码实现:

int QuickSortPart(int* a, int left, int right)
{
    int key = a[left];
    int pit = left;
    while(left < right)
    {
        if (left < right && a[left] >= key)
        {
            right--;
        }
        a[pit] = a[right];
        pit = right;
        
        if (left < right && a[right] <= key)
        {
            left++;
        }
        a[pit] = a[left];
        pit = left;
    }
    a[pit] = key;
    return pit;
}

void QuickSort(int* a, int begin, int end)
{
    if (begin >= end)
    {
        return NULL;
    }
    
    int key = QuickSortPart(a, begin, end);
    QuickSort(a, begin, key - 1);
    QuickSort(a, key + 1, end);
}

双指针法

1.首先我们设置一个prev和cur分别指向第一和第二个位置。

2.当cur处的值找到小于Key的值时,prev++然后交换cur处的数据和prev处的数据,cur++

3.当cur处的值大于等于key的值时,cur++,prev不变换。

4.当cur走出数组时结束排序。

5.交换prev处的值和key的值。

双指针法思想其实也是将小的值放到前面,大的值放到后面,只是通过双指针进行操作。

每次找到小的值就和prev++后处的值进行交换,这是将小的值向左移动,prev处永远可以保持是小于key的值,如果cur处的值大于key就将cur++。

继续用 6-8-4-3-2-9-7这组数据为例

在这里插入图片描述

代码如下:

void QuickSortPart(int* a, int left, int right)
{
    int key = left;
    int prev = left;
    int cur = left + 1;
    while(cur <= right)
    {
           if (a[cur] < a[key] && a[++prev] != a[cur])
           {
               Swap(&a[cur], &a[prev]);
           }
           cur++;
    }
     a[key] = a[prev];
    return prev;
}
void QuickSort(int* a, int begin, int end)
{
    if (begin <= end)
    {
        return NULL;
    }
    int key = QuickSortPart(a, begin, end);
    QuickSort(a, begin, key - 1);
    QucikSort(a, key + 1, end);
}

三种方法的算法复杂度都为0(N)

快排总的算法复杂度取决于每次选择key的位置,如果每次选择的key都是中位数,如图情况。

在这里插入图片描述

此种情况下快排的算法复杂度为O(N*logN)

最坏情况是每次选择的key都为最小或最大,这种情况如图所示

在这里插入图片描述

这种情况下的算法复杂度为O(N^2)

并且如果数据量很大的情况下,由于我们在递归调用这有可能使得栈溢出。

所以针对这种最坏的情况我们进行优化,每次选择key时我们遵循“三数取中原则”,每次取最左边的数据最中间的数据还有最右边的数据,用这三个数据进行比较选择中间大小的数据当作key使用,取得key的值和最原代码最左边的数据进行交换,这样可以保证不用过多改动源代码。

代码:

//找到三数中间大小的下标
int GetMidIndex(int* a, int left, int right)
{
	int mid = left + (right - left) / 2;
	if (a[left] < a[mid])
	{
		if (a[mid] < a[right])
		{
			return mid;
		}
		else if (a[left] > a[right])
		{
			return left;
		}
		else
		{
			return right;
		}
	}
	else
	{
		if (a[mid] > a[right])
		{
			return mid;
		}
		else if (a[right] < a[left])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

//快速排序 "hoare"
int QuickSortPart1(int* a, int left, int right) 
{
	//优化三数取中
	int mid = GetMidIndex(a, left, right);
	Swap(&a[mid], &a[left]);

	int key = left;
	while (left < right)
	{
		while (left < right && a[right] >= a[key])
		{
			right--;
		}

		while (left < right && a[left] <= a[key])
		{
			left++;
		}

		Swap(&a[left], &a[right]);
	}

	Swap(&a[left], &a[key]);

	return left;
}

优化前:栈溢出

在这里插入图片描述

在这里插入图片描述

优化后:

在这里插入图片描述

对于快排还有第二种优化方式,这种方式叫做小区间优化

小区间优化的思想就是因为在快排递归到后面时大多数数据可能已经接近有序,但是我们仍然还需要对其进行区间划分然后排序,所以此时为了减少这些递归的次数,我们可以采用插入排序进行替换。

数据量小切接近有序的情况下我们大多数情况下选择插入排序。

代码如下:

void QuickSort1(int* a, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}

	if (end - begin + 1 < 13)//当递归调用到大约13个数左右就进行替换
	{
		InsertSort(a + begin, end - begin + 1);
	}
	else
	{
		int target = QuickSortPart3(a, begin, end);
		QuickSort1(a, begin, target - 1);
		QuickSort1(a, target + 1, end);
	}
}

最后我们对优化过后的快排和其他的排序性能进行比较:

10000数据量比较

在这里插入图片描述

100000数据量比较

在这里插入图片描述

非递归实现快速排序

快速排序使用递归,每一次其实都是在对空间进行划分,所以我们可以借用一种容器对其进行记录,然后进行排序。

在这里我们采用栈这种结构来对快排的空间进行记录。

代码如下:

void QuickSort(int* a, int begin, int end)
{
     Stack st;
     StackInit(&st);
     StackPush(&st, begin);
     StackPush(&st, end);
      
     while(!StackEmpty(&st))
     {
         int right = StackTop(&st);
         StackPop(&st);
         int left = StackTop(&st);
         StackPop(&st);
         
         int key = QuickPart(a, left, right);
         if (left < key - 1)
         {
             StackPush(&st, left);
             StackPush(&st, key - 1);
         }
         
         if (key + 1 < right)
         {
             StackPush(&st, key + 1);
             StackPush(&st, right);
         }
     }
    StackDestroy(&st);
}

我们一般调用函数需要建立函数栈帧,栈区在32位Linux系统下大约时8M,而堆区是动态开辟空间,在32位Linux系统下大约是2G,所以在大量数据情况下还是选用非递归的方法进行快速排序。

插入排序

直接插入排序

插入排序是将每一位的数据和前面的数据进行比较如果小于上一位就将上一位的数据向后挪一位,然后再继续与前一位比较,直到找到比其小的数据,然后将其插入到这个数据的后面。

代码:

void InsertSort(int* a, int n)
{
    for (int i = 0; i < n - 2; i++) 
    {
        int end = i;
        int tmp = a[end + 1];
        while(end >= 0) 
        {
            if (a[end] > tmp) 
            {
                a[end + 1] = a[end];
                end--;
            }
            else 
            {
                break;
            }
        }
        a[end + 1] = tmp;
    }
}

最坏的情况是需要将降序的数据排成升序,每一个数据都要进行n次比较,结果就是 1 + 2 + 3 + 4 + …… + n,所以算法复杂度就为O(N^2),由于在原数组上进行操作,只开辟了一个tmp和end所以空间复杂度为O(1)

希尔排序

希尔排序是在插入排序的基础上进行了优化,首先我们要进行预排序。

确定一个gap值,然后将每个gap差距的数据进行插入排序,这样可以尽量让数据大的数据在后面,数据小的在前面。

gap的值越大,数据大的越向后,数据小的越在前面,但是越不接近有序。

gap的值越小,越接近有序。

gap > 1时是预排序,当gap = 1时就相当于直接插入排序。

void ShellSort(int* a, int n)
{
   int gap = n;
   while(gap > 1)
   {
       gap = gap / 3 + 1;
       for (int i = 0; i < n - gap; i++)
       {
           int end = i;
           int tmp = a[end + gap];
           while (end >= 0)
           {
               if (a[end] > tmp)
               {
                   a[end + gap] = a[end];
                   end -= gap;
               }
               else
               {
                   break;
               }
           }
           a[end + gap] = tmp;
       }
   }
}

希尔排序的算法复杂度较为复杂,由于gap的变化不定,在参考一些资料以后,基本上希尔排序的算法复杂度平均下来为O(N^1.3)

归并排序

归并排序

归并排序的思想就是两组有序的数列合并为一个有序的数列,不断的将问题变小,直到不能再分为止。

以下面这个数列为例,

6-3-4-5-2-9-7

在这里插入图片描述

递归实现代码如下:

void _MergeSort(int* a, int begin, int end, int* tmp)
{
	if (begin >= end)
	{
		return;
	}
	
	//找到中间点然后递归
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);

	int begin1 = begin;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = end;
	int index = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}
	//若任一段排完,就将另一端直接加到后面
	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[index++] = a[begin2++];
	}
	memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	assert(tmp);

	_MergeSort(a, 0, n - 1, tmp);

	free(tmp);
}

非递归实现代码:

//非递归归并排序
void MergeSortN(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;

			//end1 越界 修改end1
			if (end1 >= n)
			{
				end1 = n - 1;
			}

			// begin2 越界 第二个区间不存在
			if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}

			// begin2无误 end2越界只用修改end2
			if (begin2 < n && end2 >= n)
			{
				end2 = n - 1;
			}

			int index = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
		}
		memcpy(a, tmp, sizeof(int) * n);
		gap *= 2;
	}

	free(tmp);
}

非递归实现可能会产生越界,越界会分为三种情况一种是end1越界,此时begin2和end2不存在,所以只修改end1即可,第二种情况是begin2越界,此时第二个区间就不存在,第三种情况是begin2没有越界end2越界,此时只需要修改begin2。

归并排序最坏和最好的时间算法复杂度相同都为O(logN),空间复杂度为O(N)。

计数排序

计数排序属于非比较排序,采用哈希的思想进行排序,首先对于一个数组先遍历数组找到最大和最小的数据,然后就可以知道这个数组数据大小的范围,根据这个范围找到每个数出现的次数,插入到新数组中,最后就知道每个数出现的次数,然后根据从小到大的顺序取出数据,这样就完成了排序。

在这里插入图片描述

代码:

void AcountSort(int* a, int n)
{
    int max = a[0];
    int min = a[0];
    for (int i = 0; i < n; i++)
    {
        if (a[i] > a[max])
        {
            a[max] = a[i];
        }
        
        if (a[i] < a[min])
        {
            a[min] = a[i];
        }
    }
    
     int range = max - min + 1;
     int* Count = (int*)malloc(sizeof(int) * range);
     assert(range);
     memset(Count,0,sizeof(int) * range);
     
    for (int i = 0; i < n; i++)
    {
         count[a[i] - min]++;
    }
    
    int j = 0;
    for (int i = 0; i < n; i++)
    {
        while(count[i]--)
        {
            a[j++] = i + min;
        }
    }
}

排序算法稳定性及性能比对

算法稳定性是指排序前后相同的两数的相对位置没有发生变化如图所示。

在这里插入图片描述

上述算法中

  • 直接选择排序不稳定
  • 堆排序不稳定
  • 冒泡排序稳定
  • 快速排序不稳定
  • 归并排序稳定
  • 直接插入稳定
  • 希尔不稳定
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值