最全的排序算法原理以及代码实现

排序算法的实现过程


序言:最近复习了各种排序算法,由于面试对于排序算法考的比较频繁,并且在刷题过程中遇到了各种各样的排序题目,但往往都万变不离其宗,因此,专门总结了各种排序算法及其实现方法,用C语言实现了其基本原理。
本文所涉及到的算法主要有:

  1. 冒泡排序
  2. 选择排序
  3. 插入排序
  4. 希尔排序
  5. 归并排序
  6. 快速排序
  7. 快速选择
  8. 堆排序

1.冒泡排序

运行步骤:

  1. 比较相邻的两个元素,如果前一个比后一个大,则调换两个元素。依次类推,对每一对相邻的元素做相同的操作,可以得到数组末尾存放最大的元素。
  2. 针对所有元素重复第一步的操作,除去最后的元素直到没有一组元素需要比较,即可将数组从小到大排列。

/**
	* 冒泡排序
	* 最优时间复杂度 ---- 	O(n)
	* 最差时间复杂对 ----	O(n^2)
	* 平均时间复杂度 ----	O(n^2)
	* 所需辅助空间   ----   O(1)
	* 稳定性 -----------    稳定
**/

void BubbleSort(int* arr, int len) {
    for (int i = 0; i < len; i++) {
        for (int j = 0; j < len - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

2.选择排序

运行步骤:

  1. 在已知序列中寻找最小(大)的元素,放到起始位置。
  2. 从剩余数据中寻找最小(大)元素,放到第二个位置。
  3. 重复以上两步直到所有元素排列完毕。

/**
	* 选择排序
	* 最优时间复杂度 ---- 	O(n^2)
	* 最差时间复杂对 ----	O(n^2)
	* 平均时间复杂度 ----	O(n^2)
	* 所需辅助空间   ----    O(1)
	* 稳定性 -----------    稳定
**/

void SelecttionSort(int* arr, int len) {
    for (int i = 0; i < len; i++) {
        int index = i;
        for (int j = i + 1; j < len; j++) {
            if (arr[j] < arr[index]) {
                index = j;
            }
        }
        if (index == i) {
            continue;
        }
        else {
            int tmp = arr[index];
            arr[index] = arr[i];
            arr[i] = tmp;
        }
    }
}

3.插入排序

运行步骤:

  1. 取出第一个元素作为已排序元素。
  2. 取出下一个元素,在已排序的元素中从后往前遍历。
  3. 如果当前元素大于新元素,将新元素与前一个元素比较。
  4. 重复步骤3,直到找到一个元素比新元素小或者等于。
  5. 将新元素插入到当前元素的下一位。
  6. 重复2-5步。

/**
	* 插入排序
	* 最优时间复杂度 ---- 	O(n)
	* 最差时间复杂对 ----	O(n^2)
	* 平均时间复杂度 ----	O(n^2)
	* 所需辅助空间   ----    O(1)
	* 稳定性 -----------    稳定
**/

void InsertSort(int* arr, int len) {
    int i, j;
    int target;
    for (i = 1; i < len; i++) {
        target = arr[i];
        for (j = i; j > 0 && arr[j - 1] > target; j--) {
            arr[j] = arr[j - 1];
        }
        arr[j] = target;
    }
}

4.希尔排序

运行步骤:

  1. 确定一个固定步长,将原始序列分割成数个等长的子序列。
  2. 对子序列进行插入排序。
  3. 缩短步长,重复以上两步,直到步长为1则排序完成。

/**
	希尔排序
	* 最优时间复杂度 ----	与步长相关
	* 最差时间复杂度 ----	O(nlogn)
	* 平均时间复杂度 ----	O(nlogn)
	* 所需辅助空间 ------	 O(1)
	* 稳定性 -----------    不稳定
**/

void ShellSort(int* arr, int len) {
    int i, j, k, increasement;
    int tmp;
    for (increasement = len / 2; increasement > 0; increasement /= 2) {
        for (i = 0; i < increasement; i++) {
            for (j = i + increasement; j < len; j += increasement) {
                if (arr[j] < arr[j - increasement]) {
                    tmp = arr[j];
                    for (k = j - increasement; k >= 0 && arr[k] > tmp; k -= increasement) {
                        arr[k + increasement] = arr[k];
                    }
                    arr[k + increasement] = tmp;
                }
            }
        }
    }
}

int main() {
    int a[10] = {8, 2, 1, 5, 9, 7, 4, 6, 3, 0};
    ShellSort(a, 10);
    for (int i = 0; i < 10; i++) {
        printf("%d\n", a[i]);
    }
    return 0;
}

5.归并排序

算法步骤:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。

排序运行步骤:

  1. 将一个序列分成两个子序列。
  2. 对两个子序列递归的进行归并。
  3. 将两个子序列合并成一个有序的序列。

/**
	归并排序
	* 最优时间复杂度 ----	O(nlogn)
	* 最差时间复杂度 ----	O(nlogn)
	* 平均时间复杂度 ----	O(nlogn)
	* 所需辅助空间 ------	 O(n)
	* 稳定性 -----------    稳定
**/

void Merge(int* arr, int* tmp, int start, int mid, int end) {		//归并函数
    int length = 0;													//辅助数组长度
    int i_start = start;											//左起始
    int i_end = mid;												//左结尾
    int j_start = mid + 1;											//右起始
    int j_end = end;												//右结尾
    while (i_start <= i_end && j_start <= j_end) {					//将两个子序列合并到TMP数组中
        if (arr[i_start] < arr[j_start]) {
            tmp[length++] = arr[i_start];
            i_start++;
        }
        else {
            tmp[length++] = arr[j_start];
            j_start++;
        }
    }
    while (i_start <= i_end) {
        tmp[length++] = arr[i_start++];
    }
    while (j_start <= j_end) {
        tmp[length++] = arr[j_start++];
    }
    for (int i = 0; i < length; i++) {								//将tmp数组拷贝到arr中
        arr[i] = tmp[i];
    }
}

void MergeSort(int* arr, int start, int end, int* tmp) {
    if (start >= end) {
        return;
    }
    int mid = (start + end) / 2;
    MergeSort(arr, start, mid, tmp);
    MergeSort(arr, mid + 1, end, tmp);
    Merge(arr, tmp, start, mid, end, tmp);
}

int main() {														//测试用例
    int a[8] = {5, 2, 3, 6, 1, 0, 7, 4};
    int b[8] = {0};
    Merge(a, 0, 7, b);
    for (int i = 0; i < 8; i++) {
        printf("%d\n", a[i]);
    }
    return 0;
}


6.快速排序

运行步骤:

  1. 从序列中定义一个基准量,一般选择序列的第一个元素作为基准量。
  2. 将基准值保存并定义两个指针分别指向第一个元素和最后一个元素。
  3. 开始遍历,将序列中比基准值小的元素放到前面,比基准值大的元素放到序列后面。
  4. 将基准元素放到两指针相等的地方插入(注意边界条件)。
  5. 递归的进行步骤1-3,当双指针不在出现交换后,即可得到排序后的数组。

/**
	快速排序
	* 最优时间复杂度 ----	O(nlogn)
	* 最差时间复杂度 ----	O(n^2)
	* 平均时间复杂度 ----	O(nlogn)
	* 所需辅助空间 ------	 O(logn)
	* 稳定性 -----------    不稳定
	
	* 本题所展示的遍历方法是将基准值保存到base变量中,然后从后开始寻找小于基准值的数arr[right]并将其赋给数组第一个数(即基准
	* 所在位置),然后左边的指针left+1,此时right的值并没有变化,开始寻找第一个比基准值大的数,并将left的值赋给right。此后  	* 一直遍历,直到left == right跳出循环。
	* 注意,这种方法的边界条件的判定left < right,即left == right跳出的原因是,这种算法是每找到一个比基准值大(小)的数时	* 就通过赋值的方式放到后面(前面),但不改变当前值,因此当left++(right++)后,如果没有再出现比基准值大(小)的值的话,则	* 当left == right的时候,此时指向的值是最后一个已经赋值过的值,将基准值赋值给它,就能得到一轮排序后的序列。
    
    ** 在下一个快速选择算法中我会采用另一种原理相同但实现方式不同的方法,其边界条件也不同。
**/

int Partition(int* arr, int len, int start, int end) {
    if (len <= 0 || arr == NULL || start < 0 || end >= len || end <= 0) {
        return -1;
    }
    int base = arr[start];			//定义基准量
    int left = start;			//定义左右两个指针
    int right = end;
    while (left < right) {		//注意边界条件,这种方法当left == right时跳出
        while (left < right && arr[right] >= base) { 		//如果右边的值大于基准值且范围未越界,则向前遍历
            right--;
        }
        if (left < right) {			//此时跳出循环意味着找到了一个值小于基准值,判断是否越界
            arr[left] = arr[right];
            left++;
        }
        while (left < right && arr[left] < base) {
            left++;
        }
        if (left < right) {
            arr[right] = arr[left];
            right--;
        }
    }
    arr[left] = base;
    return left;
}

void QuickSort(int* arr, int len, int start, int end) {
    if (start == end) {								//递归出口,当两个指针相等的时候退出
        return;
    }
    int index = Partition(arr, len, start, end);
    if (index > start) {
        QuickSort(arr, len, start, index - 1);		//对前半部分进行快速排序
    }
    if (index < end) {
        QuickSort(arr, len, index + 1, end);		//对后半部分进行快速排序
    }
}

int main() {
    int arr[10] = {8, 2, 1, 5, 9, 7, 4, 6, 3, 0};
    QuickSort(arr, 10, 0, 9);
    for (int i = 0; i < 10; i++) {
        printf("%d\n", arr[i]);
    }
    return 0;
}

6.1快速选择

运行方法:

​ 快速选择是基于快速排序的一种变种算法,并不是排序算法,而是一种查找算法,常用于查找序列中最大(最小)的第K个数。

基本原理和快速排序一样,都是先寻找基准值,然后将大的放后面,小的放前面,不同的地方在于,快速选择并不需要双边递归,因为他不需要排序,只需要进入边进行递归查找。


​ 快速选择和快速排序的原理基本一致,但是我这里用了两种不同的遍历方法实现,上文中使用的是赋值法,这一次我使用的是交换法。两种方法都可以实现Partition函数,但是边界条件不同。我想了很久的边界条件问题,特此记录。

该方法需要定义一个辅助交换函数,与上文中的方法的主要区别在于上文使用的方法:当找到第一个比基准值大(小)的数a时,立刻赋值给第一个数,由于此时第一个数是基准值,已经保存在base变量中,此时数组第一个元素其实是无用的元素,因此可直接赋值。赋值后,a就变成了那个没用的数值。

而快速选择中使用的方法是:查找第一个比基准值大的值后,去查找第一个比基准值小的值,确定两个的位置后,进行交换操作,然后两个指针移动。此时边界条件需要定为left <= right,原因是这种搜索方法并不需要保存基准值start == 0,因此,left指针指向的是left+1,如果此时待排序数组arr中只有两个数,那么此时会出现left == right的情况,如果边界条件为left < right,此时会直接跳出循环。


/**
	快速选择
	* 最优时间复杂度 ----	O(n)
	* 最差时间复杂度 ----	O(n^2)
	* 平均时间复杂度 ----	O(n)
	* 所需辅助空间 ------	 O(logn)
	* 稳定性 -----------  /
**/
void swap(int* arr, int a, int b) {
    int tmp;
    tmp = arr[a];
    arr[a] = arr[b];
    arr[b] = tmp;
}

int Partition(int* arr, int len, int start, int end) {
    int left = start + 1;
    int right = end;
    while (left <= right) {
        while (left <= right && arr[right] > arr[start]) {
            right--;
        }
        while (left <= right && arr[left] < arr[start]) {
            left++;
        }
        if (left <= right) {
            swap(arr, left++, right--);
        }
    }
    swap(arr, start, right);
    return right;
}

int findKthLargest(int* nums, int numsSize, int k) {
    if (nums == NULL || numsSize == 0) {
        return 0;
    }
    int start = 0;
    int end = numsSize - 1;
    while (1) {
        int position = Partition(arr, numsSize, start, end);
        if (position > k - 1) {
            end = position - 1;
        }
        else if (position == k - 1) {
            return nums[position];
        }
        else {
            start = position + 1;
        }
    }
}

7.堆排序

大根堆:所有父节点的值大于子节点

最小堆:所有父节点的值都小于子节点

运行过程:

  1. 构造初始堆,将无序序列构造出一个大根堆或者最小堆(一般大根堆实现从大到小排列,最小堆实现从小到大排列)。
  2. 将堆顶元素与最后一个元素互换,使得最后一个元素最大(最小),并将其分离出来。
  3. 再将剩余的元素构造出一个大根堆(最小堆),重复上述步骤,得到排列的序列。

/**
	堆排序(leetocode原题《求最大的K个数》)
	* 最优时间复杂度 ----	O(nlogn)
	* 最差时间复杂度 ----	O(nlogn)
	* 平均时间复杂度 ----	O(nlogn)
	* 所需辅助空间 ------	 O(1)
	* 稳定性 -----------    不稳定
**/
void swap(int* arr, int a, int b) {
    int tmp = arr[a];
    arr[a] = arr[b];
    arr[b] = tmp;
}

int heapify(int* arr, int i, int len) {
    int lc = 2 * i + 1;		//左子节点
    int lr = 2 * i + 2;		//右子节点
    int max = i;			//保存根节点
    if (lc < len && arr[lc] > arr[max]) {
        max = lc;
    }
    if (lr < len && arr[lr] > arr[max]) {
        max = lr;
    }
    if (max != i) {			//如果此时原本的根节点不是最大值
        swap(arr, max, i);
        heapify(arr, max, len);		//递归,构筑大根堆
    }
}

void buildheap(int arr, int len) {
    int last_node = len - 1;
    int parent = (last_node - 1) / 2;
    int i;
    for (i = parent; i >= 0; i--) {
        heapify(arr, i, len);
    }
}

void heapSort(int arr, int len) {
    buildheap(arr, len);
    int i;
    for (i = len - 1; i >= 0; i--) {
        swap(arr, i, 0);				//将根节点和末节点交换,把最大值取出
        heapify(arr, 0, i);				//将剩下的序列排列
    }
}

int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize) {
    heapSort(arr, arrSize);
    * returnSize = k;
    int* nums = (int*)malloc(sizeof(int) * k);
    int i;
    for (i = 0; i < k; i++) {
        nums[i] = arr[i];
    }
    return nums;
}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值