Andy Niu �����ĵ�

Andy Niu

Andy Niu Help  1.0.0.0
排序算法

变量

 冒泡排序
 
 插入排序
 
 希尔排序
 
 选择排序
 
 快速排序
 
 堆排序
 
 归并排序
 

详细描述

变量说明

冒泡排序
1、冒泡排序,外部循环,执行趟数,第一趟,对集合相邻两个比较,大的数向下沉,结束后,最大的数在最下面,
    第二趟,从头部开始,尾部减1,找出次大的数。
2、时间复杂度:
    平均情况【n*n】
    最好情况【n*n】
    最坏情况【n*n】
4、辅助空间:
    1
5、稳定的排序算法
template <typename T>
void BubbleSort(std::vector<T>& vec)
{
    for(int i=0;i<vec.size();++i)
    {
        for(int j=0;j<vec.size()-i-1;++j)
        {
            if(vec[j] > vec[j+1])
            {
                Swap(vec[j],vec[j+1]);
            }
        }
    }
}
1、上面的冒泡排序存在问题,比如已经有序的情况,0 1 2 3 4 5,每一趟都需要遍历两两比较。
2、可以优化,如果这一趟没有执行swap,可以断定第二趟也不会执行swap,因此优化如下:
template <typename T>
void BubbleSort_2(std::vector<T>& vec)
{
    bool isNeedContinue = true; // 初始化需要进行下一趟

    for(int i=0;i<vec.size()&&isNeedContinue;++i)
    {
        isNeedContinue = false; // 假定不需要下一趟

        for(int j=0;j<vec.size()-i-1;++j)
        {
            if(vec[j] > vec[j+1])
            {
                Swap(vec[j],vec[j+1]);
                isNeedContinue = true; // 存在交换,还要进行下一趟
            }
        }
    }
}
堆排序
1、堆排序分为两步:构建堆和去掉头部,重新调整堆。
2、堆排序的思路:堆是逻辑上的完全二叉树,通过下标确定父子关系,a[k]的两个孩子a[2k+1]和a[2k+2].
    堆化需要知道当前下标和集合的长度,对当前下标和子节点堆化。
3、构建堆,从第一个非叶子结点,开始,向前递进。
    去除头部,重新调整堆,当前下标是0,长度是原来的长度减1。
4、时间复杂度:
    平均情况【nlogn】
    最好情况【nlogn】
    最坏情况【nlogn】
5、辅助空间:
    1
6、不稳定的排序算法。
// curIndex 非叶子结点的下标,对它和子节点堆化
// 堆化的长度
void HeapAdjust(vector<int>& vec, int curIndex,int len)
{
    while(curIndex<len)
    {
        int leftChildIndex  = 2*curIndex+1;
        int rightChildIndex = 2*curIndex+2;

        if(leftChildIndex < len) //左孩子存在
        {
            if(rightChildIndex < len) //右孩子存在
            {
                // 两个孩子中找到一个大的
                if(vec[leftChildIndex] >= vec[rightChildIndex]) // 左孩子大
                {
                    // 和左孩子比较
                    if(vec[curIndex] >= vec[leftChildIndex]) // 堆正确
                    {
                        break;
                    }
                    else // 把左孩子提拔上来
                    {
                        swap(vec[curIndex],vec[leftChildIndex]);
                        curIndex = leftChildIndex;
                    }
                }
                else
                {
                    // 和右孩子比较
                    if(vec[curIndex] >= vec[rightChildIndex]) // 堆正确
                    {
                        break;
                    }
                    else // 把右孩子提拔上来
                    {
                        swap(vec[curIndex],vec[rightChildIndex]);
                        curIndex = rightChildIndex;
                    }
                }
            }
            else
            {
                // 和左孩子比较
                if(vec[curIndex] >= vec[leftChildIndex]) // 堆正确
                {
                    break;
                }
                else // 把左孩子提拔上来
                {
                    swap(vec[curIndex],vec[leftChildIndex]);
                    curIndex = leftChildIndex;
                }
            }
        }
        else
        {
            break;
        }   
    }
}

void HeapSort(vector<int>& vec)
{
    // 从最后一个非叶子节点,向前,堆化
    int start = vec.size()/2 -1; // 最后一个非叶子结点
    for(int i = start; i>=0; --i)
    {
        HeapAdjust(vec,i,vec.size());
    }

    // 头和尾交换,从头到尾部前一个堆化
    for(int i =0;i< vec.size()-1;++i) // 最后一个元素不需要堆化
    {
        swap(vec[0],vec[vec.size()-i-1]);
        HeapAdjust(vec,0,vec.size()-i-1);
    }
}
希尔排序
1、对于插入排序,如果已经基本有序,算法效率很高。
2、所谓基本有序,也就是对于整个序列,小的数在前面,中间的数在中间,大的数在后面。
    如何使它基本有序呢?分组,跳跃式的分组。考虑9个元素,分成3组,分别为:
    a[0],a[3],a[6]
    a[1],a[4],a[7]
    a[2],a[5],a[8]
    每一组插入排序,就使得基本有序。然后缩小间隔,直到为1
3、也就是说,希尔排序有三层循环,外层循环缩小间隔,因为是插入排序,前面的一个元素有序,
    中间循环从每个分组的第二个元素开始,遍历a[3],a[4]...,内层循环记录当前下标和元素,和target比较,
    挖前面的数,填后面的坑。
    虽然希尔排序有三层循环,但是效率高。外层循环缩小,下降很快,中间是n,内层循环跳跃是 n/interval,
    interval不是常数,会变化。希尔排序的时间复杂度 小于 n*n
4、时间复杂度:
    平均情况【nlogn到n*n】
    最好情况【n^1.3      】n的1.3次方
    最坏情况【n*n        】
5、辅助空间:
    1
6、不稳定的排序算法。
void ShellSort(vector<int>& vec)
{
    int interval = vec.size();
    do
    {
        interval = interval/3+1;

        for(int i = interval;i<vec.size();++i) // 从每个分组的第二个元素
        {
            int curIndex = i;   //记录当前下标
            int target = vec[i];//记录当前元素,挖数,留下一个坑
            // 没有走到分组的第一个元素,并且目标小
            while(curIndex>=interval && target<vec[curIndex-interval]) 
            {
                vec[curIndex]=vec[curIndex-interval]; // 挖前面的数,填后面的坑
                curIndex-=interval; //分组的前一个元素
            }
            vec[curIndex] = target; // 补平遗留的坑
        }

    }while(interval>1); // 缩小间隔,直到为1
}
归并排序
1、归并排序的思路:使用递归,调用MSort,MSort内部使用一个临时的集合,对前一段归并,对后一个归并,然后Merge
2、时间复杂度:
    平均情况【nlogn】
    最好情况【nlogn】
    最坏情况【nlogn】
3、辅助空间:
    n
4、稳定的排序算法。
// 对于 aVec, 范围[first,mid)和范围[mid,last)都是有序的,将它们归并bVec[first,last)
void Merge(vector<int>& aVec,vector<int>& bVec,int first,int mid,int last)
{
    int i=first; // 前一段的开始下标
    int j=mid;   // 后一段的开始下标
    int k=first; // 目标集合的开始下标

    while(i<mid && j<last) // 两段都没有走到结尾
    {
        if(aVec[i]<aVec[j]) // 前面的小
        {
            bVec[k++] = aVec[i++]; // 从前一段取元素
        }
        else
        {
            bVec[k++] = aVec[j++]; // 从后一段取元素
        }
    }

    // while退出后,只有一段还有剩余元素,哪个段的最大值大,哪个段有剩余元素

    // 复制前一段的剩余元素
    while(i<mid)
    {
        bVec[k++] = aVec[i++];
    }

    // 复制后一段的剩余元素
    while(j<last)
    {
        bVec[k++] = aVec[j++];
    }
}

// vecRef和vec是同一个集合的引用,把vec的[first,last)归并到vecRef
void MSort(vector<int>& srcVec,vector<int>& dstVec,int first,int last)
{
    if(first == last-1)
    {
        dstVec[first]=srcVec[first];
    }
    else
    {
        int mid = (first+last)/2;

        // 栈上分配一个临时的集合
        vector<int> tmpVec;
        tmpVec.resize(dstVec.size());

        // vec的[first,mid) 归并到vecCpy的[first,mid)
        MSort(srcVec,tmpVec,first,mid);

        // vec的[mid,last) 归并到vecCpy的[mid,last)
        MSort(srcVec,tmpVec,mid,last);      
        
        // vecCpy的[first,mid)和[mid,last)有序,进行Merge到vecRef
        Merge(tmpVec,dstVec,first,mid,last);
    }
}

void MergeSort(vector<int>& vec)
{
    MSort(vec,vec,0,vec.size());
}
1、使用递归,导致空间上的浪费,每次都要一个临时对象。
2、递归的思路是先自上而下,再自下而上。
3、能不能直接自下而上,相邻合并,再相邻合并。
void MergePass(vector<int>& srcVec,vector<int>& dstVec,int interval)
{
    int i =0;
    int len = dstVec.size();
    while(i< len-2*interval+1)
    {
        Merge(srcVec,dstVec,i,i+interval,i+2*interval);
        i = i+2*interval;
    }

    if(i<len-interval) // 最后剩余2个序列,长度不同
    {
        Merge(srcVec,dstVec,i,i+interval,dstVec.size());
    }
    else // 最后剩余1个序列
    {
        for(int j = i;j<len;++j)
        {
            dstVec[j] = srcVec[j];
        }
    }
}

void MergeSort_2(vector<int>& vec)
{
    vector<int> tmpVec;
    tmpVec.resize(vec.size());
    
    int i = 1;
    while(i<vec.size())
    {
        MergePass(vec,tmpVec,i); // 把vec相邻,间隔为1的两块,归并到tmpVec

        i*=2;
        MergePass(tmpVec,vec,i); // 把tmpVec相邻,间隔为2的两块,归并到vec

        i*=2; // 为下一趟做准备
    }
}
快速排序
1、快速排序,调用递归方法。因为是折半递归,效率并不差。如果是线性递归,比如斐波那契数列,效率很低。
2、快速排序思路:递归调用,前后相遇,结束递归。没有相遇,先从前面挖个数,留下一个坑,然后循环,
    一个循环里,后面挖个数填前面的坑,然后前面挖个数,填后面的坑,循环直到相遇。
    相遇之后,跳出循环,填上遗留的坑,对前后两块递归调用。
3、时间复杂度:
    平均情况【nlogn】
    最好情况【nlogn】,每次都刚好分成均匀的两块
    最坏情况【n*n  】,每次都分成极端不均衡的两块,1和n-1个
4、辅助空间:
    递归造成的栈空间使用,递归的深度最好情况 logn,最坏情况n,因此辅助空间 logn到n
5、比较和交换是跳跃式的,因此是不稳定的排序算法。
template <typename T>
void QSort(std::vector<T>& vec,int left,int right)
{
    int i = left;   // 左边
    int j = right-1;// 右边

    if(i >= j) // 已经相遇,结束递归
    {
        return;
    }

    T target(vec[i]); // 从前面挖个数,留下一个坑

    while(i<j) // 前后夹击,直到相遇
    {               
        while(i<j && vec[j]>target) // 从后向前,找出一个小的
        {
            --j;
        }
        if(i<j) // 没有相遇
        {
            vec[i++] = vec[j]; //从后面挖个数,填上前面的坑
        }

        while(i<j && vec[i]<target) // 从前向后,找出一个大的
        {
            ++i;
        }
        
        if(i<j) // 没有相遇
        {
            vec[j--]=vec[i]; //从前面挖个数,填上后面的坑
        }   
    }

    vec[i] = target; // 填上遗留的坑
    QSort(vec,left,i);
    QSort(vec,i+1,right);
}

template <typename T>
void QuickSort(std::vector<T>& vec)
{           
    QSort(vec,0,vec.size());
}
插入排序
1、插入排序,前面的元素有序,当前元素进行插入。外部循环,记录当前下标,把当前元素挖走,留下一个坑,
    对于前面有序的子序列,从后向前,找出比它小的,比它大,从前面挖数,填后面的坑,直到头部或者找到位置,
    最后填上遗留的坑。
2、时间复杂度:
    平均情况【n*n】
    最好情况【n  】,已经有序,每次都不需要从前面挖数,填后面的坑。
    最坏情况【n*n】,倒序排列,每次都要从前面挖数,填后面的坑,一直到最前面。
3、辅助空间:
    1
4、稳定的排序算法。
template <typename T>
void InsertSort(std::vector<T>& vec)
{
    for(int i =1;i<vec.size();++i)
    {
        int curIndex = i; // 记录当前的下标
        T target(vec[i]); // 挖个数,留下一个坑

        while(curIndex >0 && target<vec[curIndex-1]) // 当前元素小,向前移动
        {
            vec[curIndex] = vec[curIndex -1]; // 从前面挖数,填后面的坑
            --curIndex; // 递进
        }

        vec[curIndex] = target; // 填上遗留的坑
    }
}
选择排序
1、选择排序,外部循环,假定当前下标就是最小值的下标,内部循环从下一位开始遍历,找出一个最小值的下标。
2、内部循环退出,和假定不一致,进行交换。
3、时间复杂度:
    平均情况【n*n】
    最好情况【n*n】
    最坏情况【n*n】
    因为,假定当前下标的元素就是最小值,必须遍历内部循环才确定。
4、辅助空间:
    1
5、稳定的排序算法。
template <typename T>
void SelectSort(std::vector<T>& vec)
{
    for(int i =0;i<vec.size();++i)
    {
        int minIndex = i; // 假定当前下标就是最小值的下标

        for(int j =i+1;j<vec.size();++j)
        {
            if(vec[minIndex]>vec[j])
            {
                minIndex = j; // 找出最小值的下标                   
            }
        }

        if(minIndex != i) // 和假定不一致,交换元素
        {
            Swap(vec[minIndex],vec[i]);
        }
    }
}
Copyright (c) 2015~2016, Andy Niu @All rights reserved. By Andy Niu Edit.