排序算法--C语言

欢迎来到本期节目- - -
排序

前言

本期将介绍一些经典的排序算法,如果您有补充,欢迎各种奇思妙想!

排序算法的++稳定性++ :
在一组待排序的数据中,如果存在相同key元素(排序算法的关键比较元素)的两个及以上数量的数据在经过该算法的排序之后,它们的相对位置并没有改变,那么该排序算法稳定,反之,不稳定.

旅客们请注意,您乘坐的由 一脸懵逼站 始发 至 豁然开朗站 的N次列车即将发车了!

选择排序

时间复杂度:O(N^2^)
空间复杂度:O(1)
稳定性:不稳定

动图演示:

算法思路:

|-----------------|
| 从左往右排序,每趟排好一个数; |

|-----------------------------------|
| 在待排数组中,遍历一遍找出最小值,然后和待排数组的第一个位置交换; |

cpp 复制代码
void Select_Sort(int* arr,int n)
{
	assert(arr);
	for(int begin = 0; begin < n-1;begin++)
	{
	int mini = begin;
	for(int j = mini+1;j<n;j++)
	{
		if(arr[j]<arr[mini])
		{
			mini = j;
		}
	}
	swap(&arr[begin],&arr[mini]);
	}
}

选择排序的优缺点:
优点:空间复杂度低,算法简单
缺点:时间复杂度较高,效率低,不适合大规模数据排序。


冒泡排序

时间复杂度:O(N^2^)
空间复杂度:O(1)
稳定性:稳定

动图演示:

算法思路:

|----------------|
| 从右往左排,每趟确定一个数; |

|--------------------------------------|
| 遍历待排数组,遍历过程中,将相邻元素的关系通过交换使得前一个小于后一个; |

cpp 复制代码
void Bubble_Sort(int* arr, int n)
{
	assert(arr);
	int flag = 0;
	for (int i = 0; i < n - 1; i++)
	{
		flag = 1;
		for (int j = 0; j < n - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				swap(&arr[j], &arr[j + 1]);
				flag = 0;
			}
		}
		if (flag == 1)
		{
			break;
		}
	}

}

冒泡排序优缺点:
优点:算法简单,空间复杂度低
缺点:时间复杂度较高,效率低


插入排序

时间复杂度:O(N^2^)
空间复杂度:O(1)
稳定性:稳定

动图演示:

算法思路:

|----------------------------------------------|
| 从左往右排序,每趟排序确定一个数; 每趟排序将待排序数组的第一个数往已排序的数组中插入; |

|------------------------------------------------------------------------|
| 比较方式是将本次要插入的元素inserted_num 与已排序的数组依次从后往前比,如果大于inserted_num,则留出空位,否则插入; |

cpp 复制代码
void Insert_Sort(int* arr, int n)
{
	for(int i = 0; i < n-1; i++)
	{
		int end = i;
		int inserted_num = arr[end+1];
		while(end>=0 && arr[end]>inserted_num)
		{
			arr[end+1] = arr[end];
			end--;
		}
		arr[end+1] = inserted_num;
	}
}

插入排序的优劣势:

优势:
排序稳定;
数据量较小是,效率较高;
当数据接近有序时,效率较高;

劣势:
不适合大规模数据排序;


希尔排序

时间复杂度:O(N^1.3^)
空间复杂度:O(1)
稳定性:不稳定

动图演示:

算法思路:

|---------------|
| 该算法是对插入排序的优化; |

|--------------------------|
| 通过预排使得数组接近有序,最后进行一次插入排序; |

|-----------------------------|
| 每次预排是将原数组分为多个小组,每个小组进行插入排序; |

|-----------------------------------------|
| 每次预排的小组数量少于上一次,直到最后只分一个小组,既对整个数组进行插入排序; |

cpp 复制代码
void shellsort(int* nums,int n)
{
    int gap = n;	//gap既代表本次预排的小组数量,也代表了小组中相邻元素的距离

    while(gap > 1)
    {
        gap = gap/3+1;
        for(int i = 0; i < n-gap; i++)
        {
        	//为了对应动图演示,这里稍微和插入排序不一样,不是用插入的方式,而是用交换的方式,但是效果是一样的.
            int end = i;	
            while(end >= 0 && nums[end] > nums[end+gap])
            {
                swap(&nums[end],&nums[end+gap]);
                end-=gap;
            }
        }
    }
}

希尔排序的优劣势:

优势:
适合中等规模数据排序;
当数据初始状态在一定程度的无序时,效率高于其它高级排序;

劣势:
排序不稳定;
性能受到增量序列的影响,性能不稳定;


堆排

时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定

动图演示:

算法思路:

|-----------------------|
| 物理结构上是数组,逻辑结构上是完全二叉树; |

|-----------------------------------|
| 堆的性质:任何一个根节点都大于等于(或者小于等于)左右子树的节点; |

|-------------------------------|
| 根据该性质通过控制数组下标的方式来维护堆,既建堆和调整堆; |

cpp 复制代码
void AdjustDown(int* arr, int n, int parent)
{
    int child = parent*2+1;
    while(child < n)
    {
        if(child+1 < n && arr[child+1] > arr[child])
            child++;
        
        if(arr[child] > arr[parent])
        {
            swap(&arr[child],&arr[parent]);
            parent = child;
            child = parent*2 + 1;
        }
        else
            break;
    }
}

void Heap_sort(int* arr, int n)
{
    //建堆---O(N)
    for(int i = (n-2)/2; i >= 0; i--)
    {
        AdjustDown(arr,n,i);
    }

    //堆排序---O(N*logN)
    for(int i = n-1; i > 0; i--)
    {
        swap(&arr[0],&arr[i]);
        AdjustDown(arr,i,0);
    }
}

堆排的优劣势:

优势:
性能稳定,不依赖初始状态;
适合大规模数据排序;

劣势:
排序不稳定;
当重复元素较多时,性能不如其它高级排序;


归并排序

时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定性:稳定

动图演示:

算法思路:

|-------------------------------------------------------------------------|
| 该算法采用分治的思想,将一段待排序的数据区间,对半分成左右两份区间,然后假设两份区间有序(类似二叉树的后序遍历),转化成合并两段有序区间问题; |

|-------------------------|
| 在合并两段有序区间时,需要额外的空间进行存储; |

递归版:

cpp 复制代码
void _mergesort(int* nums,int* tmp,int left,int right)	//左闭右闭区间
{
    if(left >= right)
        return;
    
    int mid = left+(right-left)/2;
    _mergesort(nums,tmp,left,mid);		//由于除2问题,注意死循环问题!
    _mergesort(nums,tmp,mid+1,right);

    int begin1 = left;
    int end1 = mid;
    
    int begin2 = mid+1;
    int end2 = right;

    int t = left;

    while(begin1 <= end1 && begin2 <= end2)
    {
        if(nums[begin1] <= nums[begin2])
            tmp[t++] = nums[begin1++];
        else
            tmp[t++] = nums[begin2++];
    }

    while(begin1 <= end1)
    {
        tmp[t++] = nums[begin1++];
    }

    while(begin2 <= end2)
    {
        tmp[t++] = nums[begin2++];
    }

    memcpy(nums+left,tmp+left,(right-left+1)*sizeof(int));
}

void Merge_sort(int* nums,int numsSize)
{
    int* tmp = (int*)malloc(sizeof(int)*numsSize);
	if(tmp == NULL)
	{
		perror("malloc fail:");
		return;
	}
    _mergesort(nums,tmp,0,numsSize-1);

    free(tmp);
    tmp = NULL;
}

非递归版:
该版本类似于直接实现递归版的回归过程;

cpp 复制代码
void Merge_sort(int* nums,int n)
{
    int* tmp = (int*)malloc(sizeof(int)*n);
    if(tmp == NULL)
    {
        perror("malloc fail:");
        return;
    }

    
    int gap = 1;    //gap表示每趟排序中每个小组的个数,最后一个小组可能小于gap
    while(gap < n)
    {
        for(int i = 0; i < n; i+=2*gap) //每次跳过两个小组,因为每次排序排的是两个小组
        {
            int begin1 = i;
            int end1 = i+gap-1;

            int begin2 = i+gap;
            int end2 = i+2*gap-1;

            if(begin2 >= n)		//处理越界问题
                break;
            if(end2 >= n)
                end2 = n-1;

            int t = begin1;

            while(begin1 <= end1 && begin2 <= end2)
            {
                if(nums[begin1] <= nums[begin2])
                    tmp[t++] = nums[begin1++];
                else
                    tmp[t++] = nums[begin2++];
            }

            while(begin1 <= end1)
                tmp[t++] = nums[begin1++];
            while(begin2 <= end2)
                tmp[t++] = nums[begin2++];

            memcpy(nums+i,tmp+i,sizeof(int)*(t-i));
        }
        gap*=2;
    }

    free(tmp);
    tmp = NULL;
}

归并排序的优劣势:

优势:
排序稳定;
适合大规模数据;

劣势:
空间复杂度较高;


快排

时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定
动图演示:

算法思路:

|-----------------------------------------------------|
| 选取一个key值,通过单趟排序,将key值位置确定,然后使用相同方式对key的左区间和右区间进行排序; |

单趟排序第一种方式:
霍尔版:

cpp 复制代码
int _quicksort1(int* nums,int begin,int end)	//左闭右闭
{
	int keyi = begin;
	int left = begin;
	int right = end;

	while(left < right)
	{
		while(left < right && nums[right] >= nums[keyi])
			right--;
		while(left < right && nums[left] <= nums[keyi])
			left++;
		
		swap(&nums[left],&nums[right]);
	}

	swap(&nums[left],&nums[keyi]);
	keyi = left;

	return keyi;
}

单趟排序第二种方式:
挖坑版:

cpp 复制代码
int _quicksort2(int* nums,int begin,int end)	//左闭右闭
{
	int key = nums[begin];
	int left = begin;
	int right = end;

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

	nums[left] = key;

	return left;
}

单趟排序第三种方式:
前后指针版:

cpp 复制代码
int _quicksort3(int* nums,int begin,int end)	//左闭右闭
{
	int keyi = begin;
	int prev = begin;
	int cur = prev+1;

	while(cur <= end)
	{
		if(nums[cur] < nums[keyi] && ++prev != cur)
			swap(&nums[cur],&nums[prev]);
			
		cur++;
	}

	swap(&nums[prev],&nums[keyi]);
	keyi = prev;

	return keyi;
}

接下来只要采用分治的思想就可以将整个数组排序完成;

cpp 复制代码
void Quick_sort(int* nums, int begin,int end) 	//左闭右闭
{
	if(begin >= end)
		return;
	
	int keyi = _quicksort3(nums,begin,end);//3种方法随便选

	Quick_sort(nums,begin,keyi-1);
	Quick_sort(nums,keyi+1,end);
}

在理想情况下,key值如果是每段区间的中值,那么该算法的递归深度为logN,既空间复杂度为logN,时间复杂度为N*logN ; 但实际上,当数组已经有序或接近有序时,该算法的空间复杂度增加到O(N),时间复杂度增加到O(N^2^) ,导致效率大大降低; 所以此时选key时,可以加上一个三端取中的函数,尽可能减少这种情况的发生;

bash 复制代码
int GetMidIndex(int* nums,int begin,int end)
{
	int mid = begin+(end-begin)/2;
	if(nums[begin] > nums[end])
	{
		if(nums[begin] < nums[mid])
			return begin;
		else if(nums[end] > nums[mid])
			return end;
		else 
			return mid;
	}
	else
	{
		if(nums[begin] > nums[mid])
			return begin;
		else if(nums[end] < nums[mid])
			return end;
		else
			return mid;
	}
	
}

但是这在一些场景下,依然会有栈递归深度太深的风险,所以我们还可以通过小区间优化降低深度,以此提高效率;

cpp 复制代码
void Quick_sort(int* nums, int begin,int end) 	//左闭右闭
{
	if(begin >= end)
		return;
	if(end-begin+1 < 16)	
	{
		Insert_Sort(nums+begin,end-begin+1);
		return;
	}
	
	int midi = GetMidIndex(nums,begin,end);
	swap(&nums[begin],&nums[midi]);
	
	int keyi = _quicksort3(nums,begin,end);

	Quick_sort(nums,begin,keyi-1);
	Quick_sort(nums,keyi+1,end);
}

虽然这已近提高了快排的效率,但是当重复的数据较多时,该排序算法的效率又会下降;
所以这里我们了解一下
三路划分:

cpp 复制代码
void Quick_sort(int* nums, int begin,int end) 	//左闭右闭
{
	if(begin >= end)
		return;
	if(end-begin+1 < 16)	
	{
		Insert_Sort(nums+begin,end-begin+1);
		return;
	}
	
	int midi = GetMidIndex(nums,begin,end);
	swap(&nums[begin],&nums[midi]);
	
	//三路划分
	int key = nums[begin];
	
	//left和right维护值为key的区间
	int left = begin;
	int right = end;
	int cur = begin+1;
	
	while(cur <= right)
	{
		if(nums[cur] < key)					//小于key和左边的key交换	,小的扔到左边,此时cur的值为key
			swap(&nums[cur++],&nums[left++]);
		else if(nums[cur] > key)			//大于key的和right指向的交换, 大的扔到右边,此时cur不知道大小
			swap(&nums[cur],&nums[right--]);
		else		//值为key,继续走
			cur++;
	}

	Quick_sort(nums,begin,left-1);
	Quick_sort(nums,right+1,end);
}

虽然三路划分可以在数据重复度高时提高排序性能,但是相较于两路划分,数据重复度不高时,性能又下降了,所以总的来说,想要用好快排得看场景;

除以上写法之外,也可以使用非递归,效率不变,因为用的是栈模拟递归调用;
非递归

cpp 复制代码
void Quick_sort(int* nums, int begin,int end) 	//左闭右闭
{
	stack st;
	STInit(&st);
	
	STPush(&st,end);
	STPush(&st,begin);

	while(!STEmpty())
	{
		int left = STTop(&st);
		STPop(&st);
		int right = STTop(&st);
		STPop(&st);

		int keyi = _quicksort3(nums,left,right);	//这里是两路划分,也可以三路划分,结合情况分析.
		if(keyi-begin > 1)
		{
			STPush(&st,keyi-1);
			STPush(&st,begin);
		}
		if(end-keyi > 1)
		{
			STPush(&st,end);
			STPush(&st,keyi+1);
		}
	}

	STDestroy(&st);
}

快排的优劣势:

优势:
性能高效;
适用性广;

劣势:
排序不稳定;
受数据初始状态影响,性能可能下降,性能不稳定;


计数排序

时间复杂度:O(N+range)
空间复杂度:O(range)
稳定性:稳定

动图演示:

算法思路:

|------------------------------------|
| 该算法采用哈希映射的策略,将数组的元素映射到下标,是非常高效的算法; |

|-----------------------------|
| 但是局限性也很明显,该算法只适用于数据集中的整数排序; |

cpp 复制代码
void Count_sort(int* nums,int n)
{
    int min = nums[0];
    int max = nums[0];

    for(int i = 1; i < n; i++)
    {
        if(min > nums[i])
            min = nums[i];
        if(max < nums[i])
            max = nums[i];
    }

    int range = max-min+1;
    int *tmp = (int*)calloc(range,sizeof(int));
    for(int i = 0; i < n; i++)
    {
        tmp[nums[i]-min]++;			//映射
    }

    int j = n-1;
    for(int i = range-1; i >= 0; i--)
    {
        while(tmp[i]--)
        {
            nums[j--] = min+i;		//还原,既排序
        }
    }
}

计数排序的优劣势:

优势:
排序稳定;
时间复杂度取决于range,如果range小于N,则效率高于所有其它排序;
无需比较,直接下标映射;

劣势:
空间复杂度高;
只适用于整数且范围较集中的数据;


希望该片文章对您有帮助,请点赞支持一下吧😘💕

相关推荐
炸鸡配泡面1 分钟前
12.10 C语言作业3
c语言·c++·算法
虾球xz23 分钟前
游戏引擎学习第41天
学习·算法·游戏引擎
羽墨灵丘1 小时前
排序算法(4):希尔排序
数据结构·算法·排序算法
小殷要努力刷题!1 小时前
每日一刷——12.10——学习二叉树解题模式(1)
java·学习·算法·leetcode·二叉树·二叉树的建立
gz7seven1 小时前
将分类数据划分为训练集、测试集与验证集
人工智能·算法·分类·数据划分·训练集·验证集·测试集
IT古董1 小时前
【机器学习】机器学习的基本分类-无监督学习-主成分分析(PCA:Principal Component Analysis)
人工智能·学习·算法·机器学习·分类
人需要PID1 小时前
【C语言练习(5)—回文数判断】
c语言·开发语言·学习·算法
小五Z2 小时前
高阶数据结构--B树&&B+树实现原理&&B树模拟实现--Java
java·数据结构·b树·算法
toto4122 小时前
红黑树和B+树
数据结构·b树·算法
chenziang12 小时前
leetcode 100 热题 三数字之和
算法·leetcode·职场和发展