用C语言实现冒泡排序,选择排序,插入排序,希尔排序,堆排序,快速排序

冒泡排序

冒泡排序的过程就是两两比较,只要前一个比后一个大的,就进行交换,每一趟只能确定一个数的最终位置。

由于每一趟只能确定一个数的最终位置,所以需要两层循环,时间复杂度为:O(N^2),空间复杂度:O(1)。

void BubbleSort(int* a, int n)
{
    for(int i = 0;i < n;i++)
    {
        //这里加了个小优化,如果这个数组已经是有序了,flag就为零,后面就不再运行,直接退出循环
        int flag = 0;
        for(int j = 1;j < n-i;j++)
        {
            if(a[i] > a[j])
            {
                flag =1;
                Swap(&a[i], &a[j]);
            }
        }
        if(flag == 0)
        {
            break;
        }
    }
}

7b95925da7da4c0aa57cfe547b09af71.gif

选择排序

选择排序的思路就是在一趟循环中选出最大的,和最小的,在一趟循环结束之后,把最大的跟在数组最后一位交换,最小的跟数组第一位进行交换时间,时间复杂度为:O(N^2),空间复杂度:O(1)

// 选择排序
void SelectSort(int* a, int n)
{
    //这里采用的是左闭右闭的方式进行循环
    int begin = 0, end = n-1;
    while(begin < end)
    {
        int mini = begin,maxi = begin;
        for(int i = begin+1;i<=end;i++)
        {
            if(a[i] > a[maxi])
            {
                maxi = i;
            }
            if(a[i] < a[mini])
            {
                mini = i;
            }
        }
        Swap(&a[begin], &a[mini]);
        //例如最大的数在第一位, 9  4 5  2 6 7 9 3 ,而我们先执行的是把最小的数,跟数组的第一位进行交换了,所以最大的数的位置会有变动,这就会导致我们最大的数,会变换不正确,所以我们要如下的一个判断
        if(maxi == begin)
        {
            maxi = mini;
        }
        Swap(&a[end], &a[maxi]);
        begin++;
        end--;
    }
}

5749bf3c5b7247cd87e45b2902c76df8.gif

 插入排序

插入排序的思想是,在[0,end]的区间内,插入一个数,使在[0,end+1]为有序,插入排序如果是在一个相对有序的数组内排序,他的时间是比冒泡排序,选择排序要快的多的,时间复杂度为O(n^2),空间复杂度:O(1)

// 插入排序
void InsertSort(int* a,int n)
{
    for(int i = 0;i<n-1;i++)
    {
        //[0,end]插入end+1,在[0,end+1]为有序,
        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;
    }
}

01f1e77a0387417e8d4f95571fec958d.gif

希尔排序

希尔排序是插入排序的升级版,升级的思路是把一个n个数的数组,分为gap组,即n/gap,一趟排序后,可以很快的把相对大的数,非常快的往后排,需要排序的数越多,gap的值就随着数的大而到小,如果gap为1,那就是插入排序,时间复杂度为O(n^1.5),要好于插入排序的O(n ^ 2),需要注意的是gap在变化的最后,必须为1.另外由于记录跳跃式的移动,希尔排序并不是一种稳定的排序方法

//希尔排序 
void ShellSort(int* a, int n)
{
    //希尔排序跟插入排序非常相似,我们把gap换成1就是插入排序
    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;
        }
    }
}

80ea502c0bca4dcdaa1f31768ccc6366.png

一趟排序下来后就变的相对有序,在后面的几趟我们继续把gap值缩小,直到1就为有序了

2f45340120a140a39b11e91421c888ea.png

 堆排序

堆排序的思想跟二叉树有很大的相似,本质的思想就是要升序就建大根堆,要降序就建小根堆,大根堆的性质就是,左右子树都小于他的根,即每个根都大于他的左右子树,小根堆的性质就是,左右子树都大于他的根,即每个根都大于他的左右子树,

//交换两个数
void Swap (int* p1,int* p2)
{
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}
//堆的向下调整
void AjustDown(int* a,int n,int parent)
{
    int Smallchild = parent *2+1;
    while(Smallchild < n )
    {
        //找出小的左右子树,跟根比较,
        if(Smallchild+1 < n && a[Smallchild+1] > a[Smallchild])
        {
            Smallchild++;
        }
        //比根大就交换,然后根再等于子树,进行下一轮的比较,
        if(a[Smallchild] > a[parent])
        {
            Swap(&a[parent], &a[Smallchild]);
            parent = Smallchild;
            Smallchild = parent*2+1;
        }
        else
        {
            break;
        }
    }
}

//堆排序
void HeapSort(int *a, int n)
{
    //大思路:选择排序,依次选数,从后往前排
    //升序:建大堆
    //降序:建小堆
    //建堆:向下调整堆O(N)
    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;
    }
}

9baddad4123547b799c214619e7cfaf5.png我们把一个数组,映射成一个二叉树,此时我们需要向下调整,以达到建成一个大根堆,向下调整的思想就是,第一次保证0~0位置大根堆结构,第二次保证0~1位置大根堆结构,第三次保证0~2位置大根堆结构...直到保证0~n-1位置大根堆结构(每次新插入的数据都与其根结点进行比较,如果插入的数比根结点大,则与根结点交换,否则一直向上交换,直到小于等于根结点,或者来到了顶端)

1.根结点下标公式:(i-1)/2(这里计算机中的除以2,省略掉小数)

2.左树下标计算公式:2*i+1

3.右树下标计算公式:2*i+2

 bfe7ee0ab5934f08ae8f4b3fc77e8c40.png

经过堆的向下调整后,每下根节点都大于他的左右子树了,这时,我们的大根堆就建好了, 由于我们要排序的是升序,所以,我们把第一个根节点,跟下标9的节点进行交换,然后我们再把堆的范围缩小1,然后再继续调整堆fc6dfc5df416439f80e46fd0fae61e7a.png

堆的每次调整,都会把最大的数,推到0下标的位置,然后我们再跟倒数第二个进行交换,再把堆调整的范围缩小-2;

ba758f95917441de91fca21135bb0ce2.png

5c50692892da40a6a80ef0ef0f009660.png

 01109751390f4e30addd3d0efbec80de.png

快速排序

快速排序的思想是,选出一个中间值key,然后用双指针,一个从右开始往左走,一个从左往右走,把大于中间值key的排在中间值key的右边,小于中间值key的排在中间值key的左边,在经过一轮交换后,再把中间值key,跟双指针相遇的位置进行交换,这时我们的一个数的最终位置就确定了,如果我们选择的中间值是从左边选择的,需要右边指针先走,以保持右边指针跟左边指针相遇的位置是中间值key的最终排序的位置,相对的如果我们选择的是右边的值为中间值key,则左边先走,这个思想是国外大佬hoare提出来的。

93d9f871f7f44e40aa1938cf19af7dab.gif

//交换两个数
void Swap (int* p1,int* p2)
{
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
    //三数取中
    int mid = GetMidIndex(a, left, right);
    Swap(&a[right], &a[mid]);
    //我们选择右边为keyi
    int keyi = right;
    while(left < right)
    {
        //由于keyi选择在右边,所以需要左边先走,以保证左跟右相撞的时候的值比keyi大
        //这里是while循环 left和right会一直变动,所以需要加上判断条件,以防left错过right
        //left找大
        while (left < right && a[left] <= a[keyi])
        {
            ++left;
        }
        //right找小
        while(left < right && a[right] >= a[keyi])
        {
            --right;
        }
        if(left < right)
            Swap(&a[left], &a[right]);
    }
    Swap(&a[left], &a[keyi]);
    //这里需要返回keyi值,以做递归分割判断
    return left;
}
//快排递归调用实现
void QuickSort(int* a, int begin, int end)
{
    if(begin >= end)
    {
        return;
    }

    //    还有大佬提出在2-^(n-3)次方时采用插入排序,能进一步提升排序时间复杂度
    if(end - begin <= 8)
    {
        InsertSort(a+begin, end-begin+1);

    }
    else
    {
        //[begin,keyi-1],keyi,[keyi+1,end];
        int keyi = PartSort1(a,begin,end);
        QuickSort(a,begin,keyi-1);
        QuickSort(a,keyi+1,end);
    }

}

这时快排的第一个思想就完成了,但有一个比较大的缺点,如果我们排的数,已经是有序了,那这个时间复杂度会很大,所以有大佬做了一个优化,我们的中间值,从左边的值跟中间的值跟右边的值,做一个判断,把这三个数中的中间值,来做中间值

//三数取中
int GetMidIndex(int* a, int left, int right)
{
    int mid = (right + left)/2;
    if(a[mid] > a[right])
    {
        if(a[mid] < a[left])
        {
            return mid;
        }
        else if(a[right] > a[left])
        {
            return right;
        }
        else
        {
            return left;
        }
    }
    else //a[mid] < a[right]
    {
        if(a[mid] > a[left])
        {
            return mid;
        }
        else if(a[right] > a[left])
        {
            return left;
        }
        else
        {
            return right;
        }
    }
}

在加入三数取中的算法优化后,我们在遇到有序的数组时就不会那么无奈了。

快排的第二个实现方法是挖坑法,相比于hoare大佬提出的思想,进行了优先,变的写代码不那么容易出错了。

//交换两个数
void Swap (int* p1,int* p2)
{
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
    //三数取中
    int mid = GetMidIndex(a, left, right);
    Swap(&a[right], &a[mid]);
    int key = a[right];
    int hole = right;
    while(left < right)
    {
        //找大的,填到坑
        while(left < right && a[left] <= key)
        {
            left++;
        }
        a[hole] = a[left];
        hole = left;
        //找小的,填到坑
        while(left < right && a[right] >= key)
        {
            right--;
        }
        a[hole] = a[right];
        hole = right;
    }
    a[hole] = key;
    return hole;
}
//快排递归调用实现
void QuickSort(int* a, int begin, int end)
{
    if(begin >= end)
    {
        return;
    }

    //    还有大佬提出在2-^(n-3)次方时采用插入排序,能进一步提升排序时间复杂度
    if(end - begin <= 8)
    {
        InsertSort(a+begin, end-begin+1);

    }
    else
    {
        //[begin,keyi-1],keyi,[keyi+1,end];
        int keyi = PartSort2(a,begin,end);
        QuickSort(a,begin,keyi-1);
        QuickSort(a,keyi+1,end);
    }

}

63fdcd30e69d471b92fdbf759d97247c.gif

快排的第三个实现方法是前后指针法,相比之前两个实现方法,这个显的更为简捷并且不容易出错。

//交换两个数
void Swap (int* p1,int* p2)
{
    int tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
    //三数取中
    int mid = GetMidIndex(a, left, right);
    Swap(&a[left], &a[mid]);
    int keyi = left;
    int prev=left,cur=left+1;
    while(cur <= right)
    {
        //在遇到比cur小的数时,并且prev++ 小于cur的时候进行交换,prev++小于cur的判断是为了两个数一样时不用进行交换
        if(a[cur] < a[keyi] && ++prev != cur)
        {
            Swap(&a[cur], &a[prev]);
        }
        //cur无论如何都会往前走
        ++cur;
    }
    //最后再把keyi的值跟prev交换
    Swap(&a[keyi], &a[prev]);
    return prev;
}
//快排递归调用实现
void QuickSort(int* a, int begin, int end)
{
    if(begin >= end)
    {
        return;
    }

    //    还有大佬提出在2-^(n-3)次方时采用插入排序,能进一步提升排序时间复杂度
    if(end - begin <= 8)
    {
        InsertSort(a+begin, end-begin+1);

    }
    else
    {
        //[begin,keyi-1],keyi,[keyi+1,end];
        int keyi = PartSort3(a,begin,end);
        QuickSort(a,begin,keyi-1);
        QuickSort(a,keyi+1,end);
    }

}

7dd3ff36aa2c44c7bf90606b6b1be055.gif

 由于快排我们前面是用递归实现的,有时数据超级大的时候,会造成栈溢出,这时我们就需要写非递归的方法实现我们的快排,非递归的快排,需要利用栈的先进后出 来实现,

// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
    ST st;
    StackInit(&st);
    StackPush(&st, left);
    StackPush(&st, right);
    //栈为空时,就没有数据了就跳出循环
    while(!StackEmpty(&st))
    {
        int end = StackTop(&st);
        StackPop(&st);
        
        int begin = StackTop(&st);
        StackPop(&st);
        
//        if(begin >= end)
//        {
//            continue;
//        }
        int keyi = PartSort1(a, begin, end);
        //如果左边的数已经大于右边的数了,就代表这个范围不存在没有必要入栈再拿出来
        if(keyi+1 < end)
        {
            StackPush(&st, keyi+1);
            StackPush(&st, end);
        }
        if(begin < keyi-1)
        {
            StackPush(&st, begin);
            StackPush(&st, keyi-1);
        }
    }
    StackDestory(&st);
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值