【数据结构与算法实验】手撕十大内部排序算法

实验目的:

在代码实践中,进一步了解十大内部排序算法及其应用场景,增强写代码的能力。

预期效果:

完成十大常用排序算法的代码编写,并使用一组数据进行实验。

实验思路:

排序算法均采用统一接口,模板为"int *算法名 (int *arr, int length);"。

测试数据为{3,7,12,4,6,2,11,1,5,8,10,7,13,9}。

实验内容:

基于比较的排序

一、冒泡排序

​ 稳定性:✓

​ 时间复杂度:平均O(n^2) 最好O(n) 最坏O(n^2)

​ 是否需要额外空间:否

​ 原理:从第一个位置开始,连续将相邻元素进行比较,大的元素排在小的元素后面,一轮结束后最后一个元素即为最大的元素,在排除最后一个元素后,重复以上步骤,直至完全有序(通过加入一个检查变量来避免不必要的循环)。

​ 使用场景:待排数据规模较小,且有序程度高。

​ 示例代码:

/* 冒泡排序 */
void bubbleSort(int *arr, int length) {
    int ifChanged = 0;
    for (int i=0; i<length; i++) {
        for (int j=0; j<length-i-1;j++) {
            if (arr[j]>arr[j+1]) {
                swap(arr,j,j+1);
                ifChanged = 1;
            }
        }
        if (!ifChanged) return;
    }
    return;
}
二、插入排序

​ 稳定性:✓

​ 时间复杂度:平均O(n^2) 最好O(n) 最坏O(n^2)

​ 是否需要额外空间:否

​ 原理:(认为第一个元素已排好)从第二个元素开始,与已排序序列中的最后一个元素比较,若已排元素小于待排元素,则继续寻找下一个待排元素。若已排元素大于待排元素,则将已排元素向后移动一位,直到找到待排元素小于等于已排元素的位置或已排元素序列头位置,将待排元素插入至该位置,重复以上步骤,直至完全有序。

​ 使用场景:待排数据规模较小,且有序程度高。

​ 示例代码:

void insertionSort(int *arr, int length) {
    for (int i=1; i<length; i++) {
        int tmp = arr[i];
        int j=i;
        for(;(j>0)&&(arr[j-1]>tmp); j--) {
            arr[j] = arr[j-1];
        }
        arr[j] = tmp;
    }
}
三、希尔排序

​ 稳定性:×

​ 时间复杂度:平均O(nlogn),具体时间复杂度所选增量序列影响

​ 是否需要额外空间:否

​ 原理:(优化版插入排序)以一定的增量序列作为间隔,进行插入排序,使待排序列一步步接近完全有序,最后进行一次直接插入排序,达到完全有序状态。

​ 使用场景:待排数据规模较大,且有序程度不高。

​ 示例代码:

/* 希尔排序 - 使用{1,3,5,7}增量序列 */
void shellSort(int *arr, int length) {
    int increasement[4] = {7,5,3,1};
    int increaseIndex = 0;
    for (int i=increasement[increaseIndex]; increaseIndex<4; i=increasement[++increaseIndex]) {
        for (int j=i; j<length; j++) {
            int tmp = arr[j];
            int k=j;
            for (;k>=i&&arr[k-i]>tmp; k-=i) {
                arr[k] = arr[k-i];
            }
            arr[k] = tmp;
        }
    }
}
四、选择排序

​ 稳定性:×

​ 时间复杂度:平均O(n^2) 最好O(n^2) 最坏O(n^2)

​ 是否需要额外空间:需要一个待排元素的辅助空间以临时存储当前最小值

​ 原理:从待排序列第一个元素开始,每次遍历找出最小值,并将其排除在外 ,在剩下待排序列中继续找最小值,直至遍历至最后一个元素。

​ 使用场景:待排数据规模较小,且有序程度不高。

​ 示例代码:

/* 选择排序 */
void selectionSort(int *arr, int length) {
    for (int i=0; i<length; i++) {
        int min = i;
        for (int j=i;j<length;j++) {
            if(arr[j]<arr[min]) {
                min = j;
            }
        }
        swap(arr,i,min);
    }
}
五、堆排序

​ 稳定性:×

​ 时间复杂度:平均O(nlogn) 最好O(nlogn) 最坏O(nlogn) (参考堆排序时间复杂度定理,此处的nlogn比真正的nlogn要小一些)

​ 是否需要额外空间:需要临时空间存储比较中的两个变量。

​ 原理:(优化版选择排序)将待排序列建为大顶堆,使顶部元素与最后一个元素交换位置,然后将最后一个元素排除在外,重复上述操作,直至遍历至第二个元素。

​ 使用场景:待排数据规模较大。

​ 示例代码:

/* 堆排序 */
void _percDown(int *arr, int root, int n) {
    // arr==待排数组,root==需要调整的根节点,n==需要调整为最大堆的长度
    int P,maxC;
    int rootValue = arr[root];
    for (P=root;P*2+1<n;P=maxC) {
        maxC = P*2+1;
        if ((maxC!=n-1)&&(arr[maxC]<arr[maxC+1])) {
            maxC++;
        }
        if (rootValue>=arr[maxC]) {
            break;
        } else {
            swap(arr,P,maxC);
        }
    }
    arr[P] = rootValue;
}
void heapSort(int *arr, int length) {
    for (int i=length/2-1; i>=0; i--) {
        _percDown(arr,i,length);
    }
    for (int i=length-1; i>0; i--) {
        swap(arr,0,i);
        _percDown(arr,0,i);
    }

}
六、归并排序

​ 稳定性:✓

​ 时间复杂度:平均O(nlogn) 最好O(nlogn) 最坏O(nlogn)

​ 是否需要额外空间:需要与待排序列大小相同的辅助序列空间

​ 原理:将待排序列从中间分为两个子序列,对两个子序列分别进行归并排序,最后将排好序的两个子序列合并(按顺序比较并排列)为最终有序的序列。

​ 使用场景:待排数据规模较大,空间较为富裕。

​ 示例代码:

/* 归并排序 */
void _mSort(int *arr, int start, int end, int *tmp) {
    int center;
    if (start<end) {
        center = (start+end)/2;
        _mSort(arr, start, center, tmp);
        _mSort(arr, center+1, end, tmp);
        int currentIndex = start;
        int L = start;
        int R = center+1;
        while (L<=center&&R<=end) {
            if (arr[L]<=arr[R]) {
                tmp[currentIndex++] = arr[L++];
            } else {
                tmp[currentIndex++] = arr[R++];
            }
        }
        while (L<=center) {
            tmp[currentIndex++] = arr[L++];
        }
        while (R<=end) {
            tmp[currentIndex++] = arr[R++];
        }
        
        for (int i=start;i<=end;i++) {
            arr[i] = tmp[i];
        }
    }
}
void mergeSort(int *arr, int length) {
    int *tmp;
    tmp = (int *)malloc(length*sizeof(int));
    if (tmp != NULL) {
        _mSort(arr,0,length-1,tmp);
        free(tmp);
    }
    else printf("Error! There is not enough space.");
}
七、快速排序

​ 稳定性:×

​ 时间复杂度:平均O(nlogn) 最好O(nlogn) 最坏O(n^2)

​ 是否需要额外空间:需要空间存储轴元素key,各个部分均有一个轴。

​ 原理:挑选出轴元素key,将比key小的放在其左边,比key大的放在其右边。之后再对key左右两边的序列进行快速排序,最终得到有序的序列。

​ 使用场景:待排数据规模足够大,需要较快的执行速度(需要数据达到一定规模,才具有速度优势,否则与简单排序相比速度甚至存在劣势),可以通过与简单排序混搭使用实现效率最大化。

​ 示例代码:

/* 快速排序 */
void _qSort(int *arr, int left, int right) {
    if (left<right) {
        int center = (left+right)/2;
        if (arr[left]>arr[center]) {
            swap(arr,left,center);
        }
        if (arr[left]>arr[right]) {
            swap(arr,left,right);
        }
        if (arr[center]>arr[right]) {
            swap(arr,center,right);
        }
        int key = arr[center];
        swap(arr,center,right-1);  // 将key藏至右边,比较只需考虑left+1~right-2
        int low = left;
        int high = right-1;
        while (1) {
            while (arr[++low]<key);
            while (arr[--high]>key);
            if (low<high) swap(arr,low,high);
            else break;
        }
        if (right-left!=1) {  // 防止当待排序列只有2个数字时,已排好的序列被交换
            swap(arr,low,right-1);  // 将key还回正确位置
        }
        _qSort(arr,left,low-1);
        _qSort(arr,low+1,right);
    }
}
void quickSort(int *arr, int length) {
    _qSort(arr,0,length-1);
}
不基于比较的排序

八、计数排序

​ 稳定性:✓/×(由代码结构决定)

​ 时间复杂度:平均O(n+k) 最好O(n+k) 最坏O(n+k)

​ 是否需要额外空间:需要额外空间,但需要空间大小由代码结构与数据规模决定

​ 原理:新建一个计数序列,遍历待排序列,每碰到待排序列的一个元素的值,就使计数序列对应下标的数加一。遍历完毕后,计数序列中的每一个值,代表了待排序列中元素出现的次数,且有顺序。最后根据计数序列进行排序即可。

​ 使用场景:待排数据规模不算大,对执行速度有一定要求。

​ 注意:一般的计数排序只能对正整数进行排序。

​ 示例代码:

/* 计数排序 (只能进行正整数的排序) */
#define MAXSIZE_FOR_COUNTINGSORT 100
void countingSort_instable(int *arr, int length) {
    int *countArr = (int *)malloc(MAXSIZE_FOR_COUNTINGSORT*sizeof(int));
    for (int i=0; i<MAXSIZE_FOR_COUNTINGSORT; i++) {
        countArr[i] = 0;
    }
    for (int j=0; j<length; j++) {
        countArr[arr[j]]++;
    }
    int currentIndex = 0;
    for (int k=0; k<length; k++) {
        while (countArr[k]-- > 0) {
            arr[currentIndex++] = k; 
        }
    }
    free(countArr);
}
void countingSort_stable(int *arr, int length) {
    int *countArr = (int *)malloc(MAXSIZE_FOR_COUNTINGSORT*sizeof(int));
    int *sortArr = (int *)malloc((length+1)*sizeof(int)); 
    for (int i=0; i<MAXSIZE_FOR_COUNTINGSORT; i++) {
        countArr[i] = 0;
    }
    for (int j=0; j<length; j++) {
        countArr[arr[j]]++;
    }
    for (int k=1; k<MAXSIZE_FOR_COUNTINGSORT; k++) {
        countArr[k] += countArr[k-1];
    }
    for (int l=length-1; l>=0; l--) {
        sortArr[--countArr[arr[l]]] = arr[l];
    }
    for (int m=0; m<length; m++) {
        arr[m] = sortArr[m];
    }
    free(countArr);
    free(sortArr);
}
九、桶排序

​ 稳定性:✓

​ 时间复杂度:平均O(n+k) 最好O(n+k) 最坏O(n^2)

​ 是否需要额外空间:需要额外空间,但需要空间大小由映射关系与数据规模决定

​ 原理:新建一个定量数组(桶),根据一定的映射关系,把数据尽量均匀的放入对应的桶中,对每个不是空的桶进行排序,将排好的数据拼接。(示例代码将使用元素与桶的下标对应的方式将数据放入桶中)

​ 使用场景:待排数据规模不算大,对执行速度有一定要求。

​ 示例代码:

/* 桶排序 */
#define MAXSIZE_FOR_BUCKET 100
void bucketSort(int *arr, int length) {
    int *bucket = (int *)malloc(MAXSIZE_FOR_BUCKET*sizeof(int));
    for (int i=0; i<MAXSIZE_FOR_BUCKET; i++) {
        bucket[i] = 0;
    }
    for (int j=0; j<length; j++) {
        bucket[arr[j]]++;
    }
    int currentIndex = 0;
    for(int k=0; k<MAXSIZE_FOR_BUCKET; k++) {
        for (;bucket[k]!=0; bucket[k]--) {
            arr[currentIndex++] = k;
        }
    }
    free(bucket);
}
十、基数排序

​ 稳定性:✓

​ 时间复杂度:平均O(nk) 最好O(nk) 最坏O(nk)

​ 是否需要额外空间:需要额外空间,但需要空间大小由映射关系,数据规模与各个位的优先级决定。

​ 原理:这里拿数字比较举例。首先取得待排序列最多要考虑的位数,建一定数量的桶,然后按照低优先级位排序,收集,再循环利用桶按高优先级位排序,再收集,直至最高优先级。

​ 使用场景:待排数据规模不算大,对执行速度有一定要求。

​ 示例代码:

/* 基数排序 */
#define MAXDIGIT_FOR_RADIXSORT 2
#define RADIX_FOR_RADIXSORT 10

typedef struct Element *ptrToElement;
struct Element {
    int key;
    ptrToElement next;
};
typedef struct BucketHead Bucket;
struct BucketHead {
    ptrToElement head;
    ptrToElement tail;
};
void radixSort(int *arr, int length) {
    Bucket bucket[RADIX_FOR_RADIXSORT];
    for (int i=0; i<RADIX_FOR_RADIXSORT; i++) {
        bucket[i].head = bucket[i].tail = NULL;
    }
    ptrToElement tempNode, tempList = NULL;
    for (int j=0; j<=length-1; j++) {
        tempNode = (ptrToElement)malloc(sizeof(struct Element));
        tempNode->key = arr[j];
        tempNode->next = tempList;
        tempList = tempNode;
    }
    for (int k=1; k<=MAXDIGIT_FOR_RADIXSORT; k++) {
        int di;
        while (tempList) {
            int tmp = tempList->key;
            for (int l=1; l<=k; l++) {
                di = tmp % RADIX_FOR_RADIXSORT;
                tmp /= RADIX_FOR_RADIXSORT;
            }
            tempNode = tempList;
            tempList = tempList->next;
            tempNode->next = NULL;
            if (bucket[di].head == NULL) {
                bucket[di].head = bucket[di].tail = tempNode;
            }
            else {
                bucket[di].tail->next = tempNode;
                bucket[di].tail = tempNode;
            }
        }
        tempList = NULL;
        for  (di=RADIX_FOR_RADIXSORT-1; di>=0; di--) {
            if (bucket[di].head) {
                bucket[di].tail->next = tempList;
                tempList = bucket[di].head;
                bucket[di].head = bucket[di].tail = NULL;
            }
        }
    }
    for (int m=0; m<length; m++) {
        tempNode = tempList;
        arr[m] = tempNode->key;
        tempList = tempList->next;
        free(tempNode);
    }
}

测试结果:

以上代码均通过自行测试,由于测试量比较大,就不逐一展示。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1.实验目的 掌握内排序,比较各种排序的优、缺点。 2 需求分析 2.1原理 2.1.1、直接排序 算法描述:经过i-1遍处理后,L[1..i-1]己排好序。第i遍处理仅将L[i]插入L[1..i-1]的适当位置,使得L[1..i]又是排好序的序列。要达到这个目的,我们可以用顺序比较的方法。首先比较L[i]和L[i-1],如果L[i-1]≤ L[i],则L[1..i]已排好序,第i遍处理就结束了;否则交换L[i]与L[i-1]的位置,继续比较L[i-1]和L[i-2],直到找到某一个位置j(1≤j≤i-1),使得L[j] ≤L[j+1]时为止。 2.1.2、冒泡排序 算法描述:核心思想是扫描数据清单,寻找出现乱序的两个相邻的项目。当找到这两个项目后,交换项目的位置然后继续扫描。重复上面的操作直到所有的项目都按顺序排好。 2.1.3、快速排序 算法描述:首先检查数据列表中的数据数,如果小于两个,则直接退出程序。如果有超过两个以上的数据,就选择一个分割点将数据分成两个部分,小于分割点的数据放在一组,其余的放在另一组,然后分别对两组数据排序。通常分割点的数据是随机选取的。这样无论你的数据是否已被排列过,你所分割成的两个字列表的大小是差不多的。而只要两个子列表的大小差不多。 2.1.4、选择排序 算法描述:首先找到数据清单中的最小的数据,然后将这个数据同第一个数据交换位置;接下来找第二小的数据,再将其同第二个数据交换位置,以此类推。 2.1.5、堆排序 (1) 基本思想:堆排序是一树形选择排序,在排序过程中,将R[1..N]看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系来选择最小的元素。 (2) 堆的定义: N个元素的序列K1,K2,K3,...,Kn.称为堆,当且仅当该序列满足特性: Ki≤K2i Ki ≤K2i+1(1≤ I≤ [N/2]) 2.1.6、希尔排序 算法描述:在直接插入排序算法中,每次插入一个数,使有序序列只增加1个节点,并且对插入下一个数没有提供任何帮助。如果比较相隔较远距离(称为增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。 2.2要求 1.本程序对以下六种常用内部排序算法进行实测比较:冒泡排序,插入排序,选择排序,希尔排序,快速排序,堆排序。 2.排序的元素的关键字为整数。用正序,逆序,不同乱序的数据作测试比较。比较的指标为有关键字参加的比较次数和关键字的移动次数。 3.程序以人机对话的形式进行,每次测试完毕显示各种比较指标值 。 2.3任务 设计一个测试程序比较几种内部排序算法的关键字比较次数和移动次数以取得直观感受。 2.4运行环境 (1)WINDOWSXP系统 (2)C++ 编译环境 3.实验方法 本实验主要是内排序,通过比较的次数和移动的次数判断排序的好坏。主要子函数的说明如下。 1.简单选择排序XuanzePaixu(); 2.冒泡排序MaopaoPaixu(); 3. 直接插入排序CharuPaixu(); 4. 快速排序KuaisuPaixu(); 5. 堆排序DuiPaixu(); 6. 希尔排序 XierPaixu(); 以上的排序算法均采用书中所用的算法。程序采用输入的时候仅输入所要的个数,具体的输入数据由程序随机产生个数,并且输出。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值