浅谈几种常见排序


这里写图片描述
注意:以下的所有排序都以排升序为例

一、插入排序

##——直接插入排序
这里写图片描述

思想:

  1. 首先将数组的第一个元素看成一个有序集合,将第一个和后一个与之比较,进行排序
  2. 然后将前两个看做一个有序集合,后一个数再和这个有序集合中的元素进行比较
  3. 以此类推,将上一层循环中排好序的 i 个元素作为一个有序集合,第 i+1个元素再与这 i 个元素进行比较排序

//直接插入排序
void InsertSort(int* a, int len)
{
	int i = 0;
	int end = 0;//控制单趟遍历的终点
	for (i; i < len - 1; i++)//将每一个元素都遍历完
	{
		end = i;
		while (end >= 0)
		{
			if (a[end] > a[end + 1])
			{
				swap(&(a[end]), &(a[end + 1]));
				end--;
			}

			else
			{
				break;
			}
		}
	}
	
}

——希尔排序

——希尔排序就是对直接插入排序的优化(先对整个数组进行一次预排序,分成多个小组,然后对这些小组分别进行直接插入排序)
这里写图片描述
希尔排序的优化:
这里写图片描述

这里写图片描述

- i走到n-gap处的位置就停止,因为每次比较的是a[i]和a[i+gap]的值,走到n-gap这个位置,就可将全部数据比较完


void ShellSort(int *a, int n)
{
	assert(a != NULL);
	int i = 0;
	int end = 0;
	int gap = n / 3 + 1;
	while (gap > 1)
	{
		for (i; i < n - gap; i++)
		{
			end = i;
			while (end >= 0)
			{
				if (a[end] > a[end + gap])
				{
					swap(&a[end], &a[end + gap]);
					end--;
				}
				else
				{
					break;
				}
			}
		}
		gap=gap / 3 + 1;//不停地对gap进行更新,缩小增量gap,使数组更加有序
	}
	InsertSort(a, n);	
}


对比:

区别选择排序希尔排序
时间复杂度O(n^2),最好是O(n)O(n1.25)~O(1.6n1.25)
空间复杂度O(1)O(1)

二、选择排序

##——直接选择排序

思想:
每次从数组中选择一个最大的(或者最小)的数,将其他的数依次与之比较,放在合适的位置上
优化:
从一个数组中分别选出最大的和最小的数,然后将最大的数放在最右边,最小的数放在最左边
再缩小范围,循环查找排序,直到数组有序为止


思路:

  1. 每次遍历选出最小(最大)的数,然后与最左(最右)的数进行交换
  2. 每选一次,范围缩小一次(left–;right++)
  3. 依次进行,直到只剩下一个数据
    这里写图片描述

代码实现:


void SelectSort(int* a,size_t n)//选择排序
{
    assert(a != NULL);

    for (int i = 0; i < n; i++)
    {
        int min = i;
        for (int j = i+1; j < n; j++)
        {
            if (a[min] > a[j])
            {
                min = j;
            }
        }
        Swap(&a[min], &a[i]);
    }
}

优化的选择排序:

采用两个指针,每次找出最小和最大的数据,分别和从最左和最右的数据进行交换


void SelectSort(int* a, int n)
{
	assert(a != NULL);
	int left = 0;
	int right = n - 1;
	int min = left;//存储最小数的下标
	int max = left;//存储最大数的下标
	int i = 0;
	for (i; i <= right; i++)
	{
		if (a[min] > a[i])//找出数组中最小的数
			min = i;
		if (a[max] < a[i])//找出数组中最大的数
			max = i;
	}
	swap(&a[min], &a[left]);

	if (max != left)
	{
		swap(&a[max], &a[right]);
	}
	else//若最大的值在最左边
	{
		//此时max位置的数,经过if语句的交换,已经变成最小值
		//而left位置的最大值,已经被交换到min位置
		max = min;
		swap(&a[max], &a[right]);
	}
	left++;
	right--;
}



——堆排序

重点内容
堆排序:向下调整+建堆+堆排
升序——建大堆;降序——建小堆
原因:以升序为例,建大堆的话,可以得出最大的数据(堆顶),然后将堆顶的数换到堆的最后一个位置a[n-1]
下次再对a[n-1]之前的n-1的元素排序,得到第二大的元素,放于a[n-2]的位置,循环下去,直到只有一个元素为止
具体步骤:
1.先找到第一个非叶子节点,下标为(n-2)/ 2
2.找出该节点中左右孩子中较大的值,并与之交换
3.交换完后,下标减一,即找第二个根节点,再做如上操作
4.依次进行,直到走到根节点
这里写图片描述


//向下调整
void AdjustDown(int *a, int n,int root)
{
	assert(a != NULL);
	int parent = root;
	int child = parent * 2 + 1;
	while (child < n)
	{
		if (child + 1 < n && a[child] < a[child + 1])
		{
			child++;//选出孩子中较大的那个
		}
		if (child + 1 < n && a[parent] < a[child])
		{
			//此时的child,已经是两个孩子中较大的那个
			swap(&a[parent], &a[child]);
		}
		parent = child;
		child = parent * 2 + 1;
	}

}


//排升序——建大堆(堆顶为最大元素)
void MakeHeap(int *a, int n)
{
	assert(a != NULL);

	//这里的n代表堆中节点的个数
	int i = (n - 2) >> 1;//找第一个非叶子节点
	for (i; i >= 0; i--)
	{
		AdjustDown(a, n, i);

	}
}

//堆排序
void HeapSort(int* a, int n)
{
	if (a == NULL || n < 2)
	{
		return;
	}
	MakeHeap(a, n);
	int end = n - 1;
	while (end >= 0)
	{
		swap(&a[end], a[0]);
		//先调整,使得倒数第二大的在堆顶位置,end再--
		AdjustDown(a, end, 0);//将前end个数进行向下调整
		end--;

	}
}


区别直接选择排序堆排序
时间复杂度O(n^2)O(n^logN)
空间复杂度O(1)O(1)


三、交换排序

##——冒泡排序

这里写图片描述

一般优化:

此种方法,优化在:循环一次,找到一个最大的放在末尾,下次再找,它的区间就会减1(由第一个倒数第二个)
以此类推,优化在减少了待找区间的大小(即内层循环的趟数)


void Bubble_Sort(int a[], int len)
{
	int flag = 0;//定义一个标志,如果数组原本有序,不用再进行排序
	
	for (int i = 0; i < len - 1; i++)
	{
		

		for (int j = 0; j < len - i - 1; j++)
		{
			if (a[j] > a[j + 1])
				Swap(&a[j], &a[j + 1]);
				flag=1;
		}
	}
	if (flag == 0)
		{
			break;
		}
		
}

void print()
{
	for (int i = 0; i < len; i++)
	{
		printf("%d", a[i]);
	}
}


更加优化:(k法)

k法更加优化在:一般优化一次区间只是减1,而k法优化一次区间可以减1(或N)

这里写图片描述


void Bubble_Sort(int a[], int len)
{
	
	int k=len-1;
	int m = 0;
	
	for (int i = 0; i < len-1; i++)
	{
		int flag = 0;
	
		for (int j = 0; j < k; j++)
		{
			if (a[j] > a[j + 1])
			{
				Swap(&a[j], &a[j + 1]);
				m = j;
				flag = 1;
			}
			if (flag == 0)
			{
				break;
			}
		}
		
		k = m;
		//不能直接 k=j,如果数组后面数据本就有序,k会前移到有序位置
        而j会走到范围的尾才结束,这样没有实现优化
	}
}



——快速排序

★选key的优化

快速排序的效率与key的值有很大的关系,如果key值选的好(每次刚好是中间数),那么时间复杂度也会很优O(lgn),但如果key值每次恰好是最大值或者最小值的话,这里的算法就是O(n*n)了。
我们上述例子都是选最右值为key值,若最右值刚好是最小值或者最大值,那么久很麻烦了,所以这里我们需要对key值进行优化

① 随机数法
顾名思义,就是每次随机数组中的一个元素作为key值
但是,我认为意义不大,因为随机选取也可能选到最大值或者最小值

//代码大家了解一下便可:
sand(time(0));
size_t index = rand()%(right - left + 1);
size_t key = a[index];

②三数取中法

1. 选出数组中的首元素、中间数、末尾数(这里中间数并非中位数),从这三个数中选出中位数,作为key值
2. 若这三个数大小相等,那key还是最大值或者最小值,出现这种情况的概率很低
3. 选出中位数,将中位数的与最右数进行交换;然后把最右数(此时已经是中位数赋给key)

int Mid(int*a, int left, int right)
{
	int mid = left + ((right - left) >> 1);
	if (a[left] > a[right])
	{
		if (a[left] < a[mid])
		{
			return left;
		}
		else if (a[right] < a[mid])
		{
			return mid;

		}
		else
		{
			return right;

		}
	}

	else
	{
		if (a[left] < a[mid])
		{
			return mid;
		}
		else if (a[right] < a[mid])
		{
			return right;
		}
		else
		{
			return left;
		}
	}
}

1. 左右指针法

思路:
1.定义两个指针begin和end,一个指向left,一个指向right
2.两个指针同时开始遍历,保证(begin小于end),指针找比key大的,end找比key小的,然后交换二者的数据
3.begin和end继续遍历,直到(begin不小于end),交换a[begin]和a[right],
4.返回begin的下标,下一次排序,以begin位置为界,分成了begin左边和begin右边两个区间
5.递归下去,直到有序为止
这里写图片描述
这里写图片描述

代码:

int LeftRightPoint(int*a, int left, int right)//左右指针法
{
	assert(a != NULL);
	int begin = left;
	int end = right;

	int mid = Mid(a, left, right);
	swap(&a[mid], &a[right]);
	int key = a[right];

	while (begin < end)
	{
		while (begin < end && a[begin] <= key)
		{
			++begin;
		}

		while (begin < end && a[end] >= key)
		{
			--end;
		}
		swap(&a[begin], &a[end]);	
	}	
	swap(&a[begin], &a[right]);
	return begin;
}

2. 挖坑法

思路:
1.与左右指针不同的是,挖坑法多了一个额外的index记录下标位置
2.不再是a[begin]与a[end]进行交换,而是a[begin]=a[index],begin=index;以及a[end]=a[index],end=index
3.其他的与左右指针法类似
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述


int Digging(int*a, int left, int right)//挖坑法
{
	assert(a != NULL);
	int begin = left;
	int end = right;
	int mid = Mid(a, left, right);
	swap(&a[mid], &a[right]);
	int key = a[right];
	//与左右指针唯一不同的就是,挖坑法多了一个下标,来存储最右(最左)元素
	int index = right;

	while (begin < end)
	{
		while (begin < end && a[begin] <= key)
		{
			++begin;
		}
		a[index] = a[begin];
		index = begin;
		while (begin < end && a[end] >= key)
		{
			--end;
		}
		a[index] = a[end];
		index = end;
	}
	a[index] = key;
	return key;
}

3. 前后指针法

思路:
1.与前面两种类似,区别在于不是两个指针从两边往中间移动,而是前后两个指针 int prev=left;int post=prev-1
2.如果a[prev]小于key,post++,如果(prev!=post) swap(&a[prev,&a[post])
3.其他的与上述两种方法类似

int PrevPost(int*a, int left, int right)
{
	int prev = left;
	int post = prev - 1;
	int mid = Mid(a, left, right);
	swap(&a[mid], &a[right]);
	int key = a[right];

	while (prev != right)
	{
		if (a[prev] > key)
		{
			++post;
			if (post != prev)
			{
				swap(&a[prev], &a[post]);
			}
		}
		++prev;
	}
	++post;
	swap(&a[prev], &a[post]);
	return post;
}

快排

void QuickSort(int*a, int left, int right)
{
		if (a == NULL || left >= right)
		{
			return;
		}
		int div = LeftRightPoint(a, left, right);
		int div = Digging(a, left, right);
		int div = PrevPost(a, left, right);

		QuickSort(a, left, div - 1);
		QuickSort(a, div + 1,right);
}

4. 用栈实现非递归的快排

void QuickSortNR(int* a, int left, int right)//快排,非递归
{
    assert(a != NULL);
    Stack s;
    StackInit(&s);
    StackPush(&s, left);
    StackPush(&s, right);
    while (StackEmpty(&s) != 0)//栈不为空
    {
        int end = StackTop(&s);
        StackPop(&s);
        int start = StackTop(&s);
        StackPop(&s);

        int div = GetMidNumber(a, start, end);

        if (start< div-1)
        {
            StackPush(&s, start);
            StackPush(&s, div - 1);
        }
        if (end > div + 1)
        {
            StackPush(&s, div + 1);
            StackPush(&s, end);
        }
    }
}


四、 归并排序

思路:
1. 采用快排分治算法的思想,没有基准,直接将数组一分为二。
2. 当二分为只剩下两个或者一个元素的时候,比较大小排序。
3. 递归回溯时,一次将两个数组进行合并,并使得合并后数组有序。
这里写图片描述

//归并排序——合并的过程
void _MergeSort(int* a, int left, int mid, int right)
{
	assert(a != NULL);
	//tmp每一次的大小不定,根据你传参确定其大小和范围
	int*tmp = (int*)malloc(sizeof(int)*(right - left + 1));
	assert(tmp != NULL);
	memset(tmp, 0, sizeof(int)*(right - left + 1));

	int index = 0;//存放tmp中元素的下标
	int begin1 = left;//定义第一个数组的范围
	int end1 = mid;

	int begin2 = mid + 1;//定义第二个数组的范围
	int end2 = right;

	//把两个数组的元素逐一比较,选出较小的放入tmp中
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}

	}

	//如果数组1 或 数组2 中还有元素,直接复制到tmp后
	if (begin1 <= end1)
	{
		while (begin1 <= end1)
		{
			tmp[index++] = a[begin1++];
		}
	}
	else
	{
		while (begin2 <= end2)
		{
			tmp[index++] = a[begin2++];
		}
	}


	//用tmp数组中的元素将原数组中无序的数据替换

	for (index=0; index < right - left + 1; index++)
	{
		//注:这里的tmp是每次的排好序的子数组,但是a的空间不变,所以将tmp的元素赋给a,是放在了a的相对位置上
		a[left+index] = tmp[index];
		
	}
	free(tmp);

}


void MergeSort(int* a, int left, int right)
{
	assert(a != NULL);
	if (left >= right)
	{
		return;
	}
	

	if (right - left + 1 > 5)//小区间优化
	{
		int mid = left + ((right - left) >> 1);
		MergeSort(a, left, mid);
		MergeSort(a, mid+1, right);
		_MergeSort(a, left, mid, right);

	}
	
	else
	{
		InsertSort(a+left, right - left + 1);

	}
}


五、计数排序

计数排序算法的原理跟哈希表的K-V模型比较相似
①遍历一遍数组,得出数组的范围range,创建一个大小为range的数组,即哈希表,初始化为全0。
②再从头开始遍历数组,数字重复出现一次,在其相应的位置对应的数值加1。
③从左到右开始遍历哈希表,将数值不为0的位置的下标存储到原数组中,且数值是多少就存储多少个 。
PS:计数排序会去除重复的元素。


六、总结篇

#——各个算法稳定性、时间复杂度的对比
这里写图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值