数据结构:排序算法

一、插入排序

时间复杂度:O(N^2)(最坏)

思想:把待排的序列逐个插入到已经排好的有序序列中,直到排完,得到一个新的有序序列

1.把第一个元素当作有序,往后插入一个数据进行排序,得到2个元素有序序列

2.前2个元素已经有序,再往后插入一个数据进行排序,得到3个元素的有序序列

3.前3个元素已经有序,再往后插入一个数据进行排序,得到4个元素的有序序列

......

4.直到排序完,得到有序的序列

一次排序:

 代码:

//插入排序
void InsertSort(int* array, int length)
{
    assert(array);

    for (int i = 0; i < length-1; i++)
    {
        int end = i;//前一个位置
        int tmp = array[end + 1];//当前位置

        while (end >= 0)
        {
            // tmp小,向后移动元素
            if (tmp < array[end])
            {
               array[end + 1] = array[end]; 
               end--;
            }
            //不处理
            else
            {
                break;
            }

        }
        // 找到比cur小的,放其后面
        array[end + 1] = tmp; 
    }
}

效果:

二、希尔排序

思路:

1.预排序

2.直接插入排序

基本思想:通过将数组分割成多个子序列来改善插入排序的效率,然后逐步减小子序列的间隔,直到最终完成排序。

步骤:1.选择间隔  2.对子序列进行插入排序  3.不断缩小间隔   4.最终排序(间隔为1)

原因:

对于直接插入排序:所给的序列越接近有序,其时间复杂度越低

我们可以先对其进行预排序,让其接近有序序列,最后再进行直接插入排序

--预排序

补充:

gap越大,大的数字越快到后面,小的数字越快到前面 ,序列越不接近有序

gao越小,大的数字越慢到后面,小的数字越慢到前面,序列越接近有序

一次子序列排序代码:(与直接插入排序差不多,改的是间隔)

这里注意i的结束位置为n-gap-1,由于tmp记录的是子序列的后一个元素,需要控制其不越界

//一次子序列排序
void ShellSort(int* a, int n) 
{

    for (int i = 0; i < n - gap; i++)
    {
	    int end = i;
	    int tmp = a[end + gap];//记录子序列中后一个数的值
	    while (end >= 0)
	    {
		    //tmp比前面的数字小,往后移动数据
		    if (tmp < a[end])
		    {
			    a[end + gap] = a[end];
			    end -= gap;//继续找前面的一个数字
		    }
		    else
		    {
			    //找完结束
			    break;
		    }
	    }
	    //将tmp放在正确的位置
	    a[end + gap] = tmp;

    }
}

一次预排序,需要将所有子序列都排序一遍

1.直接嵌套一层循环,给出子序列

子序列的组数:gap

这里可以看到可以划分为gao组子序列

 代码:

	for (int j = 0; j < gap; j++) 
	{

	}

把之前代码嵌套进去即可

2.子序列交替进行排序(不新增加循环嵌套)

代码:

//for (int i = 0; i <n - gap ; i +=gap) 改为
  for (int i = 0; i <n - gap ; i++)
 {

 }

这样可以达到交替的效果:

一次预排序

//一次预排序
void ShellSort(int* a, int n) 
{
	//1.选取间隔
	gap = 3;
	for (int i = 0; i < n-gap; i++)
	{
		int end = i;
		int tmp = a[end + gap];//记录子序列中后一个数的值
		while (end >= 0)
		{
			//tmp比前面的数字小,往后移动数据
			if (tmp < a[end])
			{
				a[end + gap] = a[end];
				end -= gap;//继续找前面的一个数字
			}
			else
			{
				//找完结束
				break;
			}
		}
		//将tmp放在正确的位置
		a[end + gap] = tmp;
	}
}


我们先给gap赋值为n

每一次预排序,gap除以3,为了确保gap最后为1,再后面加上1

希尔排序

//希尔排序
void ShellSort(int* a, int n) 
{
	//1.预排序
	int gap = n;
	//当间隔为1时结束
	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)
			{
				//tmp比前面的数字小,往后移动数据
				if (tmp < a[end])
				{
					a[end + gap] = a[end];
					end -= gap;//继续找前面的一个数字
				}
				else
				{
					//找完结束
					break;
				}
			}
			//将tmp放在正确的位置
			a[end + gap] = tmp;
		}
	}
	//直接排序
	InsertSort(a,n);

}

效果:

三、选择排序

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

思路:

1.遍历找出最小的元素(排升序)

2.将最小的元素放在序列起始位置

3.缩小范围,重复1 2

思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 

注:这里我们优化一下:

1.一次性将最小值和最大值找出

2.分别放在最左边和最右边

3.缩小范围,重复1,2

代码:

//选择排序
void SelectSort(int* a, int n) 
{
	int left, right, maxi, mini;
	left = 0, right = n - 1;
	while (left < right) 
	{
		mini = maxi = left;

		//1.找最大值和最小值的位置
		for (int i = left+1; i <= right; i++)
		{
			//找最小值
			if (a[i] < a[mini]) mini = i;
			//找最大值
			if (a[i] > a[maxi]) maxi = i;
		}
		//2.将最小值放在最左边,将最大值放在最右边
		swap(&a[left], &a[mini]);

		//排查一下最大值的位置和起始位置重复
		if (maxi == left) maxi = mini;

		swap(&a[right], &a[maxi]);
		//3.缩小范围,重复12
		left++;
		right--;
	}

}

注:需要检查maxi是否和leff重叠

 效果:

四、冒泡排序

时间复杂度:O{N^2}

思想:将序列中的数字两两比较,若前面的数字大于后面的数字就交换,这样经过一轮冒泡,最大的数字就被放在了最后面

思路:

1.将序列中的数字两两比较,判断是否交换

2.缩小范围,重复1

代码:

//冒泡排序
void BubbleSort(int* a, int n) 
{
	//每冒泡一轮就将最大的数字放在最最后面
	//10个数字冒泡9次就有序了
	
	for (int i = 0; i < n-1; i++) 
	{
		int flag = 0;
		for (int j = 0; j < n - i- 1; j++) 
		{
			//若a[j]>a[j+1]就交换
			if (a[j] > a[j + 1]) 
			{
				swap(&a[j],&a[j+1]);
				flag = 1;
			}
		}
		//说明已经有序,不需要再排序
		if (flag == 0) 
		{
			break;
		}
	}
}

 效果:

五、堆排序

时间复杂度:N*logN

思路:

1.建堆

2.将堆顶的元素与堆尾的元素交换

3.将剩下N-1个元素进行堆的调整(这里我们从堆顶开始向下调整)

这样,进行一轮23,就有一个元素从堆尾开始排好序(从大到小或从小到大)

 1.建堆

  • 升序:建大堆
  • 降序:建小堆

方式1:向上建堆,时间复杂度N*logN(从最后一个节点开始向上建堆)

	//1.建堆(向上建堆)
	for (int i = n-1;i >= 0; i--) 
	{
		AdjustUp(a,i);
	}

方式2:向下建堆,时间复杂度N(从第一个非叶子节点开始向下建堆)

	//2.建堆(向下建堆,从第一个非叶子节点开始)
	for (int i = (n - 1 - 1) / 2; i >= 0; i--) 
	{
		AdjustDown(a,n,i);
	}

补充:AdjustUp,AdjustDown

//向上调整
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		//大根堆
		if (a[child] > a[parent])
			//if (a[child] > a[parent])
		{
			swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//由上往下调整
void AdjustDown(HPDataType* a, int size, int parent)
{
	int child = parent * 2 + 1;//找到孩子节点
	//3.当走到叶子节点的时候结束调整
	while (child < size)
	{
		//1.找到较大孩子节点,并防止没有右孩子越界
		if (child + 1 < size && a[child] < a[child + 1])
		{
			child++;
		}
		//2.判断是否需要调整
		if (a[child] > a[parent])
		{
			swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}


2.利用堆删除思想排序

--假设建立的为小堆:

       进行第一次23,第一小的元素被放在堆尾

       进行第二次23,第二小的元素被放在堆倒数第二个位置

       ......

       进行第size次23,全部元素按照从大到小顺序排序好   
 

//堆排序
void HeapSort(int* a, int n)
{
	//1.建堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}
	//将堆顶的元素和最后一个元素交换
	//从该位置开始向下调整
		//若建立的是小根堆,排出的顺序为降序:最小的先放在了最后面,然后循环找出次小的放其前面
		//若建立的是大根堆,排出的顺序为升序:最大的先放在了最后面,然后循环找出次大的放其前面
	int top = 0;
	int end = n - 1;

	while (end >= 0)
	{
		//2.交换
		swap(&a[top], &a[end]);
		//3.开始向下调整
		AdjustDown(a, end, 0);
		end--;//更新end
	}
}

 效果:

六、快速排序

时间复杂度:N*logN

思想:先选择一个基准元素,然后将序列分为两组:left,right,一组都比基准元素小,一组都比基准元素大,然后递归地对子数组进行排序,最后将整个数组排序

思路:

1.选择基准元素 :通常为第一个元素

2.分割数组并重新排列:使得left都是比基准元素小的,right都是比基准元素大的

3.递归排序:将基准元素左边和右边的数组分别递归进行快速排序

4.合并结果:递归排序完成后,就会变得有序

递归排序后能有序的原因: 

1.将大问题拆分为小问题

2.当数组元素个数只有一个或没有的时候,已经排好序了,开始返回

快排代码:

//快速排序
void QuickSort(int* a, int begin, int end) 
{
	//只有一个或没有元素的时候不用再排序
	if (begin >= end)
		return;

	//排序一次
	int keyi = PortSort3(a,begin,end);

	//快排key的左边和右边
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);

}

1.hoare法代码:

int PortSort1(int* a, int begin, int end) 
{
	//1.选key
	int keyi = begin;
	int left = begin;
	int right = end;
	//2.排序
	while (left < right) 
	{
		//right先走,找小
		while (left<right && a[right] >= a[keyi]) 
		{
			right--;
		}
		//left再走,找大
		while (left < right && a[left] <= a[keyi]) 
		{
			left++;
		}
		//找到后交换left和right
		swap(&a[left],&a[right]);

	}
	//3.left和right相遇后,交换key和right
	swap(&a[keyi],&a[right]);
	//更新keyi
	keyi = right;

	return keyi;
}

效果:

 2.挖坑法

思路:

保存一下key的值

1.先选择key的位置为坑

2.right找小,找到后把该值填到坑中,此时right的位置为新坑

3.left找大,找到后把该值天到坑中,此时left的位置为新坑

重复23直到left和right相遇,将key值填入到相遇的位置

代码:

//2.挖坑法
int PortSort2(int* a, int begin, int end) 
{
	//1.保存一下key的值
	int key = a[begin];
	int left = begin;
	int right = end;
	//选坑位
	int piti = begin;
	//开始排序
	while (left < right) 
	{
		//right找小,找到填入坑中,其变为新坑
		while (left<right && a[right] >= key) 
		{
			right--;
		}
		a[piti] = a[right];//填坑
		piti = right;//right变为新坑

		//left找大,找到填入坑中,其变为新坑
		while(left < right && a[left] <= key)
		{
			left++;
		}
		a[piti] = a[left];//填坑
		piti = left;//left变为新坑
	}
	//当left与right相遇,将key填入坑中
	a[piti] = key;

	return piti;
}

 效果:

 3.前后指针法

思路:

选定key

1.pre和cur先指向同一位置,cur先走一步

2.cur走的规则:

--cur找到小,找到小后,pre先后走一步,将cur与pre的值交换,然后cur向后走一步

--cur找到大,cur继续向后走一步

注意:若找到小后,++pre和cur的位置相同,则不需要交换(交换自己没有意义)

(无论怎样cur都会向后走一步)

3.当cur走到end结束

4.将pre和key的值交换就完成了一次排序

 代码:

//前后指针
int PortSort3(int* a, int begin, int end) 
{
	int keyi, pre, cur;
	keyi = begin;
	pre = begin;
	cur = pre + 1;

	while (cur <= end) 
	{
		//1.cur找到小,pre向后走,并交换pre,cur
		if (a[cur] <= a[keyi]&&(++pre)!=cur)
		{
			swap(&a[cur],&a[pre]);
		}
		//2.cur比key大,向后走(或处理完)
		cur++;
			
	}
	//3.找完
	swap(&a[keyi],&a[pre]);
	keyi = pre;
	
	return keyi;

}

--快速排序优化

1.key的取值

key的取值会影响快速排序的效率

1.key的取值越接近中位数,每次递归排序都接近二分,效率更高

2.key的取值越接近最小值,每次递归排序的效率更低,而且冗余栈溢出

对于key的取值:

1.随机取值

2.三数取中法:取中间值 [ 第一个 中间 最后一个 (选不是最大 也不是最小的)]

思路:找到中间值的下标后,将其与begin交换,更改key值

1.取中间值

//获取中间值下标
int GetMidi(int* a, int begin, int end) 
{
	int max, min, mid,midi;
	midi = (begin + end) / 2;
	//找最大值
	max = a[begin];
	if (a[midi] > max)   max = a[midi];
	if (a[end]  > max)   max = a[end];
	//找最小值
	min = a[begin];
	if (a[midi] < min)   min = a[midi];
	if (a[end]  < min)   min = a[end];
	//获取中间值(异或)
	mid = max ^ min ^ a[begin] ^ a[midi] ^ a[end];
	//返回中间值的下标
	if (a[begin] == mid)  return begin;
	if (a[midi] == mid)   return midi;
	if (a[end] == mid)    return end;
}

2.将其与begin交换

2.小区间优化

当递归划分小区间,区间较小的时候,就不在递归排序这个小区间,用其它排序处理小区间

区间越小,递归调用次数越多 

代码:

//快速排序
void QuickSort(int* a, int begin, int end)
{
	//只有一个或没有元素的时候不用再排序
	if (begin >= end)
		return;

	//小区间用插入排序处理
	if (end - begin <= 20) 
	{
		InsertSort(a + begin,end - begin+1);
	}

	//排序一次
	int keyi = PortSort3(a, begin, end);

	//快排key的左边和右边
	QuickSort(a, begin, keyi - 1);
	QuickSort(a, keyi + 1, end);

}

--递归改非递归

思路:使用栈模仿递归实现(数据结构中的栈是动态开辟的,放在堆中,内存够大)

与层序遍历类似

1.先将数组的left和right入栈

2.再出栈获取left和right,并进行一次快速排序

3.若排序完后,其有子数组,再将子数组入栈

--检查子数组的合法性:begin < keyi -1    keyi+1 >end

4.重复 2 3 直到栈为空 就模拟完快速排序的递归

代码:

//非递归快排
void QuickSortNonR(int* a, int left, int right) 
{
	ST st;
	StackInit(&st);
	//1.先将数组的left和right入栈
	StackPush(&st,left);
	StackPush(&st, right);
	//4.直到栈为空的时候停止
	while (!StackEmpty(&st)) 
	{
		//2.出栈进行一次快排
		int right = StackTop(&st);
		StackPop(&st);
		int left = StackTop(&st);
		StackPop(&st);

		int keyi = PortSort3(a, left, right);

		//3.检查其快排一次后有没有子数组,有就入栈
		if (left < keyi - 1)
		{
			StackPush(&st, left);
			StackPush(&st, keyi - 1);
		}
		if (keyi + 1 < right)
		{
			StackPush(&st, keyi + 1);
			StackPush(&st, right);
		}
	}
	StackDestory(&st);

}

 效果:

七、归并排序

时间复杂度:N*logN

基本思想:将一个待排序的数组分成两个较小的子数组,然后分别对这两个子数组进行排序,最后将排好序的子数组合并成一个有序的数组。

基本步骤:1.拆解         2.排序         3.合并

思路:分治(与后序遍历类似)

1.将待排数组拆解成两个较小的数组,拆解到只剩下一个或没有

2.将拆解的的子数组排序

3.合并排好的子数组

 代码:

//归并排序主要思想
void _MergrSort(int* a, int begin, int end,int* tmp) 
{
	//1.拆解
	if (begin >= end) 
		return;

	int mid = (begin + end) / 2;
	_MergrSort(a, begin, mid, tmp);
	_MergrSort(a, mid+1, end, tmp);

	//2.归并
	int begin1 = begin, end1 = mid;
	int begin2 = mid+1, end2 = end;
	int j = begin;
	//比较有序数组的大小,谁小就先放进tmp里;
	while (begin1 <= end1 && begin2 <= end2) 
	{
		if (a[begin1] < a[begin2]) 
			tmp[j++] = a[begin1++];
		else
			tmp[j++] = a[begin2++];
	}
	//若一个数组先拷贝完,将每拷贝玩的数组全部拷贝进tmp中
	while(begin1 <= end1) 
		tmp[j++] = a[begin1++];

	while(begin2 <= end2)
		tmp[j++] = a[begin2++];

	//将tmp拷贝进a中
	memcpy(a+begin,tmp+begin,(end-begin+1)*sizeof(int));

}
//归并排序
void MergeSort(int* a,int n) 
{
	//额外开辟一个数组开合并
	int* tmp = (int*)malloc(n*sizeof(int));
	if (tmp == NULL) 
	{
		printf("malloc fail\n");
		exit(-1);
	}
	//开始归并
	_MergrSort(a,0,n-1,tmp);
	//释放tmp
	free(tmp);

}

效果:

--递归改非递归

思想:手动拆分并排序:

取间隔gap = 1,后依次扩大两倍取归并

 代码:

//归并排序(非递归)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	assert(tmp);
	int gap = 1;

	while (gap < n) 
	{
        // 间距为gap是一组,两两归并
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int index = i;

			//归并
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
					tmp[index++] = a[begin1++];
				else
					tmp[index++] = a[begin2++];
			}
			//若一个数组先拷贝完,将每拷贝玩的数组全部拷贝进tmp中
			while (begin1 <= end1)
				tmp[index++] = a[begin1++];

			while (begin2 <= end2)
				tmp[index++] = a[begin2++];
		}
		//将tmp拷贝给a
		memcpy(a, tmp, sizeof(int) * n);

		gap *= 2;
	}
	free(tmp);
}

越界问题的处理

这里可以看到,当序列的个数不是2^n个的时候会出现越界

1.改边界值

代码:

// end1 越界,修正
if (end1 >= n)
    end1 = n - 1;

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

// begin2 ok, end2越界,修正end2即可
if (begin2 < n && end2 >= n)
	end2 = n - 1;

 效果:

当end1越界,begin2越界,将其调整为不存在的区间,下面就不会将其归并

当只用end2越界的时候将其修正,加上归并这一个数字

使其归并完了再拷贝值 

2.越界直接不归并

代码:

//若越界直接不归并
if (begin1 >= n)
    break;
else if (begin2 >= n)
    break;
//只有end2越界,只归并一个数
else if (end2 >= n)
    end2 = n - 1;

效果:

可以选择归并一次就拷贝一次

也可以归并完了再拷贝

八、计数排序

时间复杂度:O(N,MAX-MIN+1)

基本思想:统计每个元素在待排序数组中出现的次数,然后根据这些统计信息将元素按照顺序放置到输出数组中,

思路:

1.统计元素出现次数(这里额外开辟一个数组)

2.累加次数

3.排序

预备工作:

1.得到数组的最大值和最小值   来决定额外开辟数组tmp空间的大小(max-min+1)

2.使用相对映射来累加元素出现的次数

3.遍历该数组,根据出现的次数排序

相对映射对于负数同理

代码:

计数排序
void CountSort(int* a, int n) 
{
	//1.获取数组的最大值和最小值来绝对tmp开多大
	int max = a[0],min = a[0];
	for (int i = 0; i < n; i++) 
	{
		if (a[i] > max)
			max = a[i];
		if (a[i] < min)
			min = a[i];
	}

	//开辟tmp来记录元素出现的次数
	int sz = max - min + 1;
	int* tmp = (int*)calloc(sz,sizeof(int));
	assert(tmp);

	//2.相对映射统计次数
	for (int i = 0; i < n; i++) 
	{
		tmp[a[i] - min]++;
	}
	//3.根据统计的元素排序
	int j = 0;
	for (int i = 0; i < sz; i++) 
	{
		//元素出现多少次,就拷贝过去
		while (tmp[i]--) 
		{
			a[j++] = min + i;
		}
	}
	free(tmp);
}

 效果:

 适合:数据集中,范围中等

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值