常见排序算法(附带动图帮助理解)

一、快速排序(分治思想)

1.1 基于挖坑法的单趟排序思想:形成以key为中心,左边序列都比key小,右边序列都比key大

     1.1.1  设置最左边为坑位,并保存该位置的值到key中
     1.1.2  右指针走(每次都应要求L<R),找比key小的位置
     1.1.3  找到后,将该值放入坑位中,该位置形成新的坑位
     1.1.4  L向后移动(每次都应要求L<R),找比key大的位置
     1.1.5  找到后,将该值放入坑位中,该位置形成新的坑位
     1.1.6  循环第2~5条
     1.1.7  L与R相遇,将key值放入坑位

 挖坑法动图

int partsort2(int* a, int left, int right)
{
	int mid = GetMidIndex(a, left, right);
	swap(&a[left], &a[mid]);
	int key = a[left];
	int hole = left;
	while (left < right)
	{
		// 右指针走,找比key小的位置
		while (left < right && key <= a[right])
		{
			right--;
		}
		// 找到后,将该值放入坑位中,该位置形成新的坑位
		a[hole] = a[right];
		hole = right;
		// L向后移动,找比key大的位置
		while (left < right && key >= a[left])
		{
			left++;
		}
		// 找到后,将该值放入坑位中,该位置形成新的坑位
		a[hole] = a[left];
		hole = left;
	}
	// L与R相遇,将key值放入坑位
	a[hole] = key;
	return hole;
}

挖坑单趟排序代码 

 1.2基于前后指针法的单趟排序思想:形成以key为中心,左边序列都比key小,右边序列都比key大

     1.2.1  设置前(prev)后(cur)指针与key变量,
     1.2.2  后指针cur一直走,cur停下来条件:a[cur]小于key并且cur与prev不紧跟,至少相差1个位置以上(因为从开始cur==prev+1)
     1.2.3  prev指针自增,交换a[prev]与a[cur]
     1.2.4  cur越界后,交换a[prev]与key的值

 前后指针法动图

int partsort3(int* a, int left, int right)
{
	// 1.初始,prev指向序列开头,cur指向prev的后一个位置
	// 2.cur一直找小,prev紧跟cur;cur若找到大的,prev不动
	// 3.cur继续往后走,遇到比key小的,交换cur与prev的值
	// 4.cur越界,交换prev与key的值
	int mid = GetMidIndex(a, left, right);
	swap(&a[left], &a[mid]);
	int key = a[left];
	int prev = left;
	int cur = left + 1;
	while (cur <= right)
	{
		if (a[cur] < key && ++prev!=cur) // ++prev!=cur意为把prev与cur中间没有隔着比key大的数据的情况排除
		{
			prev++;
			swap(&a[prev], &a[cur]);
		}
		cur++;
	}
	swap(&a[prev], &a[left]);
	return prev;
}

 前后指针法代码 

1.3.基于hoare法的单趟排序思想:形成以key为中心,左边序列都比key小,右边序列都比key大

     1.3.1  设置左(left)右(right)变量,left指向最左边,right指向最右边;设置key指向左边第一个
     1.3.2  right先走找比key小的数,left再走找比key大的树
     1.3.3  两边都找到后,交换left与right的值
     1.3.4  重复3.2与3.4,直到left与right相遇
     1.3.5  相遇,交换left与key的值

 hoare法动图

int partsort1(int* a,int left,int right)
{
    int mid = GetMidIndex(a,left,right);
    swap(&a[left],&a[mid]);
    
    int key = a[left];
    while(left < right)
    {
        while(left < right && a[right] >= key)
        {
            right--;
        }
        while(left < right && a[left] <= key)
        {
            left++;
        }
        if(left < right)
        {
            swap(&a[left],&a[right]);    
        }
    }
    int meet = left;
    swap(&key,&a[left]);
    return meet;
}

 hoare法代码

1.4. 基于递归实现快速排序:递归结束条件为left>=right,递归实现很像二叉树的前序遍历,每一趟都会确定一个key,通过key确定左右子树。

void quicksort(int* a,int begin,int end)
{
    if(begin >= end)
        return;
    
    int key = partsort(a,begin,end);
    quicksort(a,beign,key - 1);
    quicksort(a,key + 1,end);
}

1.5. 基于非递归实现快速排序:用栈来实现非递归版:核心在于控制key、begin、end三个变量的变化,根据递归思想,这三个值变化的规律是能分割出数组各自的段[begin,key-1]、[key]、[key+1,end]。初始begin与end先后入栈,然后获取栈顶元素放入right、随后出栈,再获取栈顶元素left、随后出栈,根据这两个数组下标调用partsort函数,确定新的key以及新的数组[begin,key-1]、[key]、[key+1,end]段,以上步骤进入循环(结束条件:栈不为空)   代码如下:

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef struct Stcak
{
    int* a;
    int top;
    int capacity;
}Stcak;

void Stackpush(Stack* ps,int x);
void Stackpop(Stack* ps);
void StackInit(Stack* ps);
void Stackdestory(Stack* ps);
int StackTop(Stack* ps);
int StackEmpty(Stack* ps);
int partsort(int* a,int left,int right);// 代码实现见上文
void quicksort(int* a,int begin,int end);

void test1()
{
    Stack ps;
    Stackpush(&ps,x);
    Stackpop(&ps);
    StackInit(&ps);
    Stackdestory(&ps);
    int tmp = StackTop(&ps);
}

void Stackpush(Stack* ps,int x)
{
    assert(ps);
    if(ps->top > ps->capacity)
    {
        int* tmp = cheackcapacity(a);
    }
    else
        ps->a[ps->top] = x;
}

void Stackpop(Stack* ps)
{    
    assert(ps);
    if(ps->top > 0)
    {
        ps->top--;
    }
}

void StackInit(Stack* ps)
{
    assert(ps);
    ps->a = NULL;
    ps->top = ps->capacity = 0;
}

void Stackdestory(Stack* ps)
{
    assert(ps);
    free(ps->a);
    ps->a = NULL;
    ps->top = 0;
    ps->capacity = 0;
}

int StackTop(Stack* ps)
{
    assert(ps);
    if(ps->top > 0)
    {
        return ps->a[a->top];
    }
}

int StackEmpty(Stack* ps)
{
    assert(ps);
	if (ps->top == 0)
	{
		return 1;
	}
	else
		return 0;
}

void quicksort(int* a,int begin,int end)
{
    Stack ps;
    StackInit(&ps);
    Stackpush(&ps,end);
    Stackpush(&ps,begin);
    while(!StackEmpty(&st))
    {
        int left = Stacktop(&ps);
        Stackpop(&ps);
        int right = stacktop(&ps);
        Stackpop(&ps);
        
        int key = partsort(a,left,right);

		if (key + 1 < right)
		{
			StackPush(&st, key + 1);
			StackPush(&st, right);
		}
		if (key - 1 > left)
		{
			StackPush(&st, left);
			StackPush(&st, key - 1);
		}  
    }
}

二、希尔排序(插入排序)

       希尔排序是直接插入排序的改进版,核心在引入gap变量,gap变量将数组数据分为gap组;

单趟排序是对这gap组的数据按照直接插入进行排序,多趟排序在于减小gap变量值进行排序,当gap==1时,此时就是直接插入排序。

 希尔排序逻辑图

void shellsort(int* a,int x)
{
    int gap = 3;
    for(int i = 0;i < gap;i++)
    {
         // 单组
         for(int j = i;j < x - gap;j += gap)
         {
             int end = j;
             int tmp = a[end+gap];
             // 单趟1个数字的插入-要插入的数小于end
             while(end >= 0)
             {
                 if(a[end+1] < tmp)
                 {
                     a[end + gap] = a[end];
                     end -= gap;
                 }
                 else
                     break;
             }
             // 要插入的数大于end
             a[end+1] = tmp;
         }  
    }
}

 希尔排序代码 

三、堆排序(选择排序)

    堆首先是完全二叉树,结构上采用动态数组的方式,所以访问堆可采用访问数组下标的方式,因此父节点与孩子结点的数组下标遵从:child = parent * 2 + 1 or parent * 2 + 2,同理:parent = (child - 1) / 2,给访问带来方便。建堆方式选择向下调整,主要原因还是时间复杂度小。另外升序建大堆、降序建小堆。思考时应该始终考虑物理结构为数组,所有操作基于数组实现,堆这种数据结构帮助我们实现操作中的某种特点操作。

     3.1  总体采用向下调整建大堆以实现数组数据的升序操作
     3.2  大堆第一个数最大的性质,交换数组第一个数与最后一个数,数组元素减减
     3.3  使用向下调整保持大堆性质
     3.4  找出次大的数与最后一个数交换,数组元素减减
     3.5  直到,数组元素为0停止

// 向下调整
void Ajustdown(int* a,int n,int parent)// 这里的parent是数组下标0
{
    int maxchild = parent * 2 + 1;
    while(maxchild > 0)
    {
        // 找当前parent下面两个孩子的最大的孩子
        if(a[maxchild] < a[maxchild + 1] && maxchild + 1 < n)
        {
            maxchild++;
        }
        if(a[maxchild] > a[parent])
        {
            swap(&a[maxchild],&a[parent]);
            parent = minchild;
            minchild = parent * 2 + 1;
        }
        else
            break;
    }
}
void HeapSort(int* a,int n)
{
    assert(a);
    // 从最后一个结点的父节点向下调整升序建大堆
    for(int i = (n - 1 - 1) / 2;i > 0;i--)
    {
        Ajustdown(a,n,i);
    }
    // 排序
    int i = 1;
    while(i < n)
    {
        swap(&a[0],&a[n - i]);
        Ajustdown(a,n - i,0);
        i++;
    }
}

堆排序代码 

四、直接插入排序

  假设数组从[0,end]有序(注意:end不是一个固定的值,通过下图可以发现end为黄色的长度),我们需要在end+1位置处进行插入,则有如下操作:

     4.1  先将end+1位置处数据保存到key,判断end+1位置处数据是否比end位置处数据大还是小
     4.2  如果小:end位置处数据依次后移,end减减
     4.3  如果大:将key插入到end+1位置处

void InsertSort(int* a,int n)
{
    for(int i = 0;i < n - 1;i++)
    {
        // 一个数据的插入排序
        int end = i;
        int key = a[end + 1];
        while(end >= 0)
        {
            // 判断end+1位置与end位置的数据大小
            // 小于情况,实际不进行插入操作,只是寻找大于的end位置
            if(key < a[end])
            {
               a[end + 1] = a[end];
               end--;                
            }
            else
                break;
        }
        // 实际进行插入,key大于end位置的值情况
        a[end + 1] = key;    
    }
}

直接插入排序代码 

五、直接选择排序

 直接选择排序优化的思想:依次遍历可以选出最大与最小的数据,最大数放最后,最小数放最前,交换之前应保存要交换的下标

void selection(int* a,int n)
{
    int begin = 0,end = n - 1;
    while(begin < end)
    {
        // 设置两个中间变量mini与maxi
        int mini = begin,maxi = end;
        for(int i = begin + 1;i <= end;i++)
        {
            // 比较
            if(a[i] < a[mini])
            {
                mini = i;
            }
            if(a[i] > a[maxi])
            {
                maxi = i;
            }
            // 交换
            swap(&a[begin],&a[mini]);
            if(mini == begin)
            {
                maxi = mini;
            }
            swap(&a[end],&a[maxi]);
            // 继续走
            begin++;
            end--;            
        }
    }
}

六、冒泡排序

 相邻两两进行比较,一趟排一个数,下一趟去除掉排好了的数。优化:若数据整体有序,一趟下来没有进行交换,则认为有序

void Bubblesort(int* a,int n)
{
    for(int i = 0;i < n-1;i++)
    {
        int flag = 0;
        for(int j = 0;j < n - i - 1;j++)
        {
            if(a[j + 1] > a[j])
            {
                swap(&a[j + 1],&a[j]);
                flag++;
            }
        }
        if(flag == 0)
            break;
    }
}

七、归并排序 

 7.1  递归版--类似与快速排序,若左区间与右区间有序,取两个区间中较小的数据,尾插到新建的数组中。若左右区间没有序,可以采用递归进行有序操作。递归操作最终将会把数组中的数据分成一个一个的数据,这样一个数据则认为有序。归并操作:每段区间归并时,相互比较,取小(大)的进行插入临时数组中,后再拷贝到原数组中,由于两段区间长度可能不同,应考虑长区间没有走完的情况

 7.2  非递归版--采用直接改循环的方式,模拟递归的分治思想,引入变量gap来控制递归思想中的区间边界(gap=1意味着组内1个元素、gap=2组内2个元素、gap=4组内4个元素...),做到从一个一个数据归并、再到两个两个数据归并、再到四个四个数据归并....

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,end1 = mid;
    int begin2 = mid + 1,end2 = end;
    int i = begin;
    while(begin1 <= end1 && begin2 <= end2)
    {
        // 两个区间比
        if(a[begin1] <= a[begin2])
        {
            tmp[i] = a[begin1];
            i++;
            begin1++;
        }
        else
        {
            tmp[i] = a[begin2];
            i++;
            begin2++;
        }
    }
    // 长区间插入
    while(begin1 <= end1)
    {
        tmp[i] = a[begin1];
        i++;
        begin1++;
    }
    // 长区间插入
    while(begin2 <= end2)
    {
        tmp[i] = a[begin2];
        i++;
        begin2++;
    }
    // 拷贝回原数组--归并哪部分就拷贝哪部分回去
    memcpy(a + begin,tmp + begin,(end - begin + 1)*sizeof(int));
}
void Mergesort(int* a,int begin,int end,int n)
{
    int* tmp = (int*)malloc(sizeof(int)*n);
    if(tmp == NULL)
    {
        exit(-1);
    }
    int begin = 0;
    int end = n - 1;
    _Mergesort(a,0,n-1,tmp);

    free(tmp);
    tmp = NULL;
}

 递归版归并排序代码

归并排序-非递归版走读代码图 

void Mergesort(int* a,int begin,int end,int n)
{
    int* tmp = (int*)malloc(sizeof(int)*n);
    if(tmp == NULL)
    {
        exit(-1);
    }
    int gap = 1;
    while(gap < n)
    {
        for(int j = 0;j < n - 1;j += 2 * gap)
        {
            int begin1 = j,end1 = j + gap - 1;
            int begin2 = j + gap,end2 = j + 2 * gap - 1;
            int i = j;
			// 第一组越界
			if (end1 >= n)
			{
				printf("[%d,%d]", begin1, n - 1);
				break;
			}
			// 第二组全部越界
			if (begin2 >= n)
			{
				printf("[%d,%d]", begin1, end1);
				break;
			}
			// 第二组部分越界
			if (end2 >= n)
			{
				// 修正一下end2,继续归并
				end2 = n - 1;
			}
			printf("[%d,%d][%d,%d] ", begin1, end1, begin2, end2);
            while(begin1 <= end1 && begin2 <= end2)
            {
                if(a[begin1] >= a[begin2])
                {
                    tmp[i] = a[begin2];
                    i++;
                    begin2++;
                }
                else
                {
                    tmp[i] = a[begin1];
                    i++;
                    begin1++;
                }
            }
            while(begin1 <= end1)
            {
                tmp[i++] = a[begin1++];
            }
            while(begin2 <= end2)
            {
                tmp[i++] = a[begin2++];
            }
			// 拷贝回原数组--归并哪部分就拷贝哪部分
			memcpy(a + j, tmp + j, (end2 - j + 1) * sizeof(int));
        }
        gap = gap * 2;
    }
    free(tmp);
    tmp = NULL;
}

  非递归版归并排序代码

八、计数排序

假如有这样的一组数据:[21,20,22,24,26,23,21,20,29] or like this [100,110,115,110,120,125,120]   or like this  [150,155,155,157,156,140,144,140].....他们都有这样的共同点那就是数据范围相对集中,这样的数据适用于计数排序。

计数排序的思想:新建数组用于存放原数组中各元素出现的次数,新建数组的下标从0到原数组中最大的数据值。for example:原数组[21,20,22,24,26,23,21,20,29],则新建数组创建从0到29的数组空间,为避免空间浪费,引入相对映射,则开辟max-min+1(29-20+1==10)个空间,映射相对位置a[i]-min.

void countsort(int* a,int n)
{
    int maxi = a[0],mini = a[0];
    for(int i = 1;i < n;i++)
    {
        if(mini > a[i])
        {
            mini = a[i];
        }
        if(maxi < a[i])
        {
            maxi = a[i];
        }
    }
    int range = maxi - mini + 1;
    int* countA = (int*)malloc(sizeof(int)*range);
    for(int i = 0;i < n;i++)
    {
        // 统计次数
        countA[a[i] - mini]++;
    }
    int j = 0;
    for(int i = 0;i < range;i++)
    {
        a[j] = i + mini;//新数组中的下标+mini等于原数组中的数据
        j++;
    }   
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值