排序算法

排序算法的时间、空间复杂度

1、冒泡排序

图解过程

1.1、2层 for 循环的起始点与终止点是什么?
1.2、冒泡时总是比较哪两个数?
1.3、有序子数组的生长情况是什么样的?
1.4、优化冒泡的flag有什么作用?

python版本

class Solution(object):
    def sortArray(self, nums):
        """
        :type nums: List[int]
        :rtype: List[int]
        """
        # i 的取值区间[0, len-1)
        # nums[len-1]是合法的,为什么 i 不取 len-1 呢?
        # 因为下面出现了nums[j] > nums[j + 1],为了避免数组越界。
        for i in range(len(nums) - 1):
        	flag = 0
        	# j 的取值区间[0, len-i-1),随着i增长,j的取值区间越来越小
        	# 有序子数组[len-i-1, len)从数组末尾向数组头生长,使得无序区间越来越小
            for j in range (len(nums) - 1 - i):
                if nums[j] > nums[j + 1]:
                    flag = 1
                    nums[j], nums[j + 1] = nums[j + 1], nums[j]
                    # 经过第一轮冒泡,max位于数组末尾,然后i++,j的取值区间被缩减了1
            
            
            if flag == 0:
            	# 如果在第i次遍历数组时发现,flag没有被更改过,说明子数组nums[0...len-1-i]是有序的。
            	# 没有泡泡可以再冒了
                break

C++版本

vector<int>& sort1(vector<int>& nums) {
	unsigned int i, j;
	// i的取值范围:[0, size-1)
	for (i = 0; i < nums.size() - 1; ++i) {
		int flag = 0;
		// j的取值范围:[0, size-1-i)
		// 有序子数组从末尾向头部生长,[0, size-1-i)会越来越小
		for (j = 0; j < nums.size() - 1 - i; ++j) {
			if (nums[j] > nums[j + 1]) {
				int tmp = nums[j + 1];
				nums[j + 1] = nums[j];
				nums[j] = tmp;
				flag = 1;
			}
		// 第一次冒泡后,max值被移动到数组末尾,++i,则j的取值[0, size-1-i)区间缩小
		// 此时数组的分布是nums[0:len-2]为无序数组,nums[len-i, len-1]为有序数组
		}
		if (!flag) {
			return nums;
		}
	}
	return nums;
}

2、选择排序

比较哪两个数?
比冒泡优在哪个方面?

从nums[0]开始,选定1个基准数,再遍历剩余数nums[1…end]且与基准数比较,然后得到最小数,把最小数挪到nums[0]。比冒泡的赋值操作少。

python版本

class Solution:
    def sortArray(nums): 
        for i in range(len(nums)): 
            min_idx = i 
            for j in range(i+1, len(nums)): 
                if nums[min_idx] > nums[j]: 
                    min_idx = j                         
            nums[i], nums[min_idx] =nums[min_idx], nums[i] 
        return nums

C++版本

/* 选择排序 */
vector<int>& sort2(vector<int>& nums) {
	int i, j, min;
	for (i = 0; i < nums.size() - 1; ++i) {
		min = nums[i];
		for (j = i + 1; j < nums.size(); ++j) {
			if (nums[j] < nums[i]) {
				min = nums[j];
				nums[j] = nums[i];
				nums[i] = min;
			}
		}
	}
	return nums;
}

3、插入排序

图解过程
第1层for表示:需要插入的次数,比数组大小少1,因为第1个数不需要插入
第2层for表示:从数组尾部向头部移动,找到插入位置

class Solution:
    def sortArray(nums):
        # 第一层for表示循环插入的遍数
        for i in range(1, len(nums)):
            for j in range(i, 0, -1):
            # 逐个比较交换,往左移动
                if nums[j] < nums[j - 1]:
                    nums[j], nums[j - 1] = nums[j - 1], nums[j]
                else:
                    break
        return nums

上面的算法的缺点:在第i-1趟插入时,需要把第i个元素插入到前面的i-1个元素中,该算法总是从i-1个元素开始逐个比较之前的每个元素,直到找到第i个元素的插入位置,这显然没有利用前面0~i-1个元素已经有序的特点

利用有序子数组,定位插入位置

优化:在0~i-1个有序元素给第i个元素寻找插入的位置时,使用二分查找法可以有效提高查找插入位置的时间效率,经过优化的插入排序称为折半插入排序,折半插入排序的时间复杂度为O(n*logn)

python版本

class Solution:
    def fineInsertIndex(self, nums, left, right, num):
        # 原理是二分法查找,但是实现细节还得根据情况修改,比如num = nums[mid]的情况该怎么处理?
        while left <= right:
            mid = left + (right - left) // 2
            if num < nums[mid]:
                right = mid - 1
            else:
                # num < nums[mid]
                # num = nums[mid]时不返回位置,而是继续二分。
                left = mid + 1
        # 思考为什么是返回left而不是right?
        return left

    def sortArray(self, nums):
        # 第一层for表示循环插入的遍数
        for i in range(1, len(nums)):
            tmp = nums[i]
            # 使用二分查找找到插入位置,前提是查找的数组是有序的
            insertIndex = self.fineInsertIndex(nums, 0, i - 1, nums[i])
            # 插入位置右边的元素依次往右移动一格
            for j in range(i, insertIndex, -1):
                nums[j] = nums[j - 1]
            # 往插入位置写入
            nums[insertIndex] = tmp
        return nums

C++版本

/* 二分插入排序 */
int binary_search(vector<int>& nums, int value, int length);				// 二分查找
vector<int>& sort3(vector<int>& nums) {
	int i, j, insert_index, tmp;

	// i = 0时为第一个数,不需要插入,i从1开始
	for (i = 1; i < nums.size(); ++i) {
		/* 获取插入位置的下标 */
		insert_index = binary_search(nums, nums[i], i);
		// 带插入的数
		tmp = nums[i];
		// 将插入位置往后的数组依次向右挪动1格
		for (j = i - 1; j >= insert_index; --j) {
			nums[j + 1] = nums[j];
		}
		// 被挪动后,插入目标值
		nums[insert_index] = tmp;
	}
	return nums;
}
/* 二分查找 */
int binary_search(vector<int>& nums, int value, int length) {
	int start = 0, end = length - 1, mid;
	
	/* 有效区间是[start, end] */
	while (start <= end) {
		mid = start + (end - start) / 2;
		
		if (nums[mid] < value) {
			start = mid + 1;
		}
		else if (nums[mid] > value) {
			end = mid - 1;
		}
		else if (nums[mid] == value) {
			/* 二分查找,当遇到连续重复的数时,想要寻找该重复数组段的右边界 */
			/* 若是想要查找左边界,则end = mid - 1 */
			/* 若是不想要取边界,则直接返回mid */
			start = mid + 1;
		}
	}
	return start;
}

4、快速排序

任意选取一个数据作为关键数据,然后将所有比它小的数都“丢”到前半段,所有比它大的数都“丢”到后半段,这个过程称为一趟快速排序。

为什么使用“丢”这个词?因为一趟“丢”完,前半段的数是较小的数,后半段的数是较大的数。但是前、后半段的子数列内部依旧是无序的,所以用“丢”这个词。

快速排序的关键:

1、左右指针靠拢的终止条件,为什么没有 == ?
2、参考点的选取。不管采用什么方法选取了参考点,都把参考点移到数组首地址,再套用朴素快排的模板即可。
3、左指针与右指针向中间靠拢,左右轮流移动。
4、基数选取方法:首元素法、3选1采样法、随机数法(平均效率更高)
5、改进版:三向切分快速排序(针对含有大量重复元素的数组而设计),3个指针
6、迭代版:需要建立栈存放准备排序的区间边界

python版本

class Solution:
    def quickSort(self, arr,left,right):
        # 递归终止条件 
        if left >= right:
            return -1
        i, j = left, right
        # 若使用其它基数选取方法,都是要将选取的基数与首元素交换
        pivot = arr[left]  

        # 一趟循环i右移1格,j左移1格   
        while i < j:
            # 从右往左过滤>=pivot的数
            while i < j and arr[j] >= pivot: 
                j -= 1
            # 碰到一个小数,“丢”到左半段
            if i < j:
                arr[i] = arr[j]
                i += 1

            # 过滤<pivot的数
            while i < j and arr[i] < pivot:
                i += 1
            # 碰到一个大数,“丢”到右半段
            if i < j:
                arr[j] = arr[i]
                j -= 1
        # 填入第i格,i即是mid
        arr[i] = pivot
        # 对2段序列快速排序
        self.quickSort(arr, left, i - 1)
        self.quickSort(arr, i + 1, right)
    
    def sortArray(self, nums):
        self.quickSort(nums, 0, len(nums) - 1)
        return nums

C++版本

/* 快速排序 */
void sort4_recursive(vector<int>& nums, int left, int right);
vector<int>& sort4(vector<int>& nums) {
	if (nums.size() < 2) return nums;

	sort4_recursive(nums, 0, nums.size() - 1);
	return nums;
}

void sort4_recursive(vector<int>& nums, int left, int right) {
	if (left >= right) return ;
	
	int value = nums[left], start = left, end = right;
	// 为什么不能出现=,因为当left==right时,需要把value存入left(right)
	// value自从被取出来后,一直没有存回去。
	// 它的源地址nums[0]在第一次while循环时就被占据了
	while (left < right) {
		// 右指针从右向左扫描,过滤掉大数,同时要注意数组越界
		while (left < right && nums[right] >= value) --right;
		// 遇到小数,while停止,此时把小数丢到左侧
		if (left < right) {
			nums[left++] = nums[right];
		}
		// 左右指针轮流移动

		// 左指针从左向右扫描,过滤掉小数,同时要注意数组越界
		while (left < right && nums[left] < value) ++left;
		// 遇到大数,while停止,此时把大数丢到右侧
		if (left < right) {
			nums[right--] = nums[left];
		}
	}
	// 把value存回去
	nums[left] = value;
	// 循环结束时,left == right
	sort4_recursive(nums, start, right - 1);
	sort4_recursive(nums, left + 1, end);

}
  • 改进版:三向切分快速排序(针对含有大量重复元素的数组而设计)
  • 相反,若没有重复元素,则获得最坏时间复杂度

三向切分的快速排序的过程是这样的:
从左到右遍历数组一次,维护3个指针,left,mid,right
1)、a[mid]小于v,将a[left]和a[mid]交换,然后将left++和mid++;
2)、a[mid]大于v,将a[right]和a[mid]交换,然后将right–;
3)、a[mid]等于v,mid++;
除非和切分元素相等,其他元素都会被交换。

在这里插入图片描述

class Solution:
    def sortArray(self, s):
        n = len(s)
        if n < 2:
            return s
        self.quickSort(s, 0, n - 1)
        return s


    def quickSort(self, s, start, end):
        if start < end:
            left, mid, right = start, start + 1, end
            pivot = s[start]
            while mid <= right:
                if s[mid] < pivot:
                    s[left], s[mid] = s[mid], s[left]
                    left += 1
                    mid += 1
                elif s[mid] == pivot:
                    mid += 1
                else:
                    s[right], s[mid] = s[mid], s[right]
                    right -= 1
            self.quickSort(s, start, left - 1)
            self.quickSort(s, right + 1, end)

5、归并排序

图解
归并排序的重点:
1、对输入数组的拆分,二分法。
2、对返回的子数组边比较边排序合并。
3、当数组较小时,改用插入排序。能增加效率。

class Solution:
    def sortArray(self, nums):
        # 递归终止条件
        if len(nums) <= 1:
            return nums
        # 将输入数列进行拆分,各自排序
        mid = len(nums) // 2
        left = self.sortArray(nums[:mid])
        right = self.sortArray(nums[mid:])
        # 下面对有序数列left和right进行合并
        res = []
        while len(left) > 0 and len(right) > 0:
            if left[0] <= right[0]:
                res.append(left.pop(0))
            else:
                res.append(right.pop(0))
        # 将剩余的有序数列直接加到末尾
        res += left
        res += right
        return res

C++版本

/* 归并排序 */
void sort5_recursive(vector<int>& nums, int left, int right);
vector<int>& sort5(vector<int>& nums) {
	if (nums.size() < 2) return nums;

	sort5_recursive(nums, 0, nums.size() - 1);
	return nums;
}
void sort5_recursive(vector<int>& nums, int left, int right) {
	// 递归终止
	if (left >= right) return;
	int mid = (left + right) / 2;

	// 对数组切片,分成2段
	vector<int> L(nums.begin() + left, nums.begin() + mid + 1);
	vector<int> R(nums.begin() + mid + 1, nums.begin() + right + 1);

	// 递归时,要假定最底层递归已经返回了正确结果,只管使用就行。
	sort5_recursive(L, 0, L.size() - 1);
	sort5_recursive(R, 0, R.size() - 1);

	// 边排序边合并2个有序数组
	int i = 0, j = 0;
	while (i < L.size() && j < R.size()) {
		if (L[i] < R[j]) {
			nums[left++] = L[i++];
		}
		else if (L[i] >= R[j]) {
			nums[left++] = R[j++];
		}
	}
	// i、j必有一个先到达边界,在原来地址上将数重新赋值
	while (i < L.size())	nums[left++] = L[i++];
	while (j < R.size())	nums[left++] = R[j++];

}

6、堆排序

图解

6.1、不使用[0]元素

约定:数组首元素[0]不使用

1、堆的整理,也叫上浮(大顶堆)或下沉(小顶堆)

  • 这是堆排序的核心,单次调整的Time = O(logn)

heapAdjust()的原理是什么?

  • 移动3个节点,使得根>max(左子树,右子树),然后递归调整左子树与右子树。

2、堆顶元素的弹出流程是什么样的?

  • 堆顶元素arr[1]与堆底元素arr[end]交换
  • max值就转移到了arr[end],接着把arr[end]排除在堆外。
  • 无序数组的大小-1,即需要调整的范围变成arr[1…end-1],因为最大值已经被取出并且放在数组末尾了。
  • 由于堆顶元素arr[1]改变了,需要再进行一次调整,这次调整时,堆顶索引不变,但是无序数组的大小需要-1。

3、堆整理需要哪3个参数?

  • 数组首地址,无序数组的大小,待整理的堆顶索引,

4、完全二叉堆的构造过程是怎么样的?

  • 从最后一个非叶节点开始,扫描至最顶部的根节点,每扫描1个都要调整最大堆。
  • 注意点:构造时的堆大小总是n,只是堆顶索引在移动,为什么?
    因为n严格来讲是指数组索引的边界。

5、最后一个非叶节点索引怎么求?

  • n / 2

6、为什么是非叶节点开始?

  • 因为非叶节点也相当于一个完全二叉堆。所以,其实是从最后一个子堆开始。
class Solution:
    def sortArray(self, nums):
        n = len(nums)
        if n < 2:
            return nums
        
        # 构建大顶堆,数组下标0不使用
        res = [-1] + nums
        # n // 2指向最后一个非叶结点
        for i in range(n // 2, 0, -1):
            self.heapAdjust(res, i, n)
            # 参数:堆地址,堆顶的索引,堆的数组边界

        # print(res)
        # 弹出堆顶元素res[1]
        for i in range(n, 0, -1):
            res[i], res[1] = res[1], res[i]
            self.heapAdjust(res, 1, i)
            # 参数:堆地址,堆顶的索引,堆的数组边界
            # print(res, i)
        return res[1:]

	# 参数:堆地址,堆顶的索引,堆的数组边界
    def heapAdjust(self, nums, root, n):
        
        child = 2 * root

        if child < n: # 存在孩子,若刚好child==n-1,则只有左孩子
            # 本层递归的任务,比较双亲与孩子结点的大小,调整双亲结点成为最大
            if child == n - 1 :
                # 只有1个孩子,并且是左孩子
                if nums[root] > nums[child]:
                    # 说明双亲比孩子大,不必调整
                    return 0
                nums[root], nums[child] = nums[child], nums[root]
                self.heapAdjust(nums, child, n)

            if child < n - 1:
                if nums[child] < nums[child + 1]:
                    # 转移指针指向大孩子
                    child += 1
                if nums[child] > nums[root]:
                    nums[root], nums[child] = nums[child], nums[root]
                    self.heapAdjust(nums, child, n)

6.2、使用[0]元素

个人觉得数组【0】不使用很别扭,只是单纯方便理解算法而已。
如何把数组【0】用上呢?代码怎么实现?

3点区别:
  • 构造大顶堆时
    最后一个子堆的索引不变,依旧是n/2;
    但是,子堆的索引 i 有效范围从[n/2, n]变成[n/2, n-1]
  • 逐个弹出堆顶元素时
    弹出个数不变,依旧是n;
    但是,堆顶元素由 [1] 变成 [0]
  • 堆调整函数的孩子索引
    左孩子索引由[root x 2] 变成 [root x 2 + 1]
class Solution:
    def sortArray(self, nums):
        n = len(nums)
        if n < 2:
            return nums
        
        
        # n // 2指向最后一个非叶结点
        for i in range(n // 2, -1, -1):		# 区别1:没有res = [-1] + nums,且数组范围i=n//2....0
            self.heapAdjust(nums, i, n)
            # 参数:堆地址,堆顶的索引,数组的边界

        # print(nums)
        # 弹出堆顶元素nums[0]				# 区别2:堆顶元素nums[0]
        for i in range(n - 1, -1, -1):		# 区别3:i=n-1....0
            nums[i], nums[0] = nums[0], nums[i]
            self.heapAdjust(nums, 0, i)
            # 参数:堆地址,堆顶的索引,数组的边界
            # print(nums, i)
        return nums

	# 参数:堆地址,堆顶的索引,数组的边界
    def heapAdjust(self, nums, root, n):
        child = 2 * root + 1				# 区别4:左孩子索引2root+1
        if child < n: # 存在孩子。若刚好child==n-1,则只有左孩子
            # 本层递归的任务,比较双亲与孩子结点的大小,调整双亲结点成为最大
            if child == n - 1 :
                # 只有1个孩子,并且是左孩子
                if nums[root] > nums[child]:
                    # 说明双亲比孩子大,不必调整
                    return 0
                nums[root], nums[child] = nums[child], nums[root]
                self.heapAdjust(nums, child, n)

            if child < n - 1:
                if nums[child] < nums[child + 1]:
                    # 转移指针指向大孩子
                    child += 1
                if nums[child] > nums[root]:
                    nums[root], nums[child] = nums[child], nums[root]
                    self.heapAdjust(nums, child, n)

C++版本

/* 堆排序 */
void heap_adjust(vector<int>& heap, int size, int root) ;
vector<int>& sort6(vector<int>& nums) {
	if (nums.size() < 2) return nums;

	// 从最后一个子堆开始,逐个子堆调整,i != 0
	for (int i = nums.size() / 2; i >= 0; --i) {
		// 传入的是数组、数组的边界、堆顶元素索引
		heap_adjust(nums, nums.size(), i);
	}
	// 调整完大顶堆后,逐个弹出堆顶元素,按顺序存入数组
	int tmp;
	for (int i = nums.size() - 1; i >= 0; --i) {
		tmp = nums[0];
		nums[0] = nums[i];
		nums[i] = tmp;
		// 传入的是数组、数组的边界、堆顶元素索引
		heap_adjust(nums, i, 0);
	}
	return nums;
}
void heap_adjust(vector<int>& heap, int size, int root) {
	int child = root * 2 + 1;
	// 判断是否存在孩子
	if (child < size) {
		// 判断是否存在2个孩子
		if (child + 1 == size) {
			// 只有一个孩子,并且是左孩子
			// 判断双亲与左孩子的大小
			if (heap[root] >= heap[child]) return;
			// 孩子比双亲大,则交换2者
			int tmp = heap[child];
			heap[child] = heap[root];
			heap[root] = tmp;
			// 双亲与孩子交换后,重新调整子堆
			// 传入的是数组、无序数组的大小、堆顶元素索引
			heap_adjust(heap, size, child);
		}
		if (child + 1 < size) {
			// 有2个孩子
			if (heap[child] < heap[child + 1]) {
				// 将child指向大孩子
				child += 1;
			}
			// 把双亲与孩子比较
			if (heap[root] < heap[child]) {
				// 孩子比双亲大,则交换2者
				int tmp = heap[child];
				heap[child] = heap[root];
				heap[root] = tmp;
				// 双亲与孩子交换后,重新调整子堆
				// 传入的是数组、无序数组的大小、堆顶元素索引
				heap_adjust(heap, size, child);
			}
		}
	}
}

6.3、堆排序步骤小结

使用数组首元素[0]的思路小结:

(1)记住堆调整函数的三个参数

数组首地址,无序数组的大小,待调整的堆顶索引

(2)构建大顶堆时,起点的最后一个非叶子节点

因此,应该从[n/2]逐个调整到[n-1],n是数组的大小

(3)弹出大顶堆时,把堆顶元素与堆底元素交换,然后把堆底元素排除出去,堆大小-1

弹出的堆顶元素总是被存放到堆底,第一次弹出最大值,第二次弹出次大值,都一次被放在数组的倒数第一位、倒数第二位…因此数组被分成前半段(无序)与后半段(升序)。

(4)堆调整函数
  • 从输入的堆顶元素索引获取孩子的索引:child = root x 2 + 1
  • 判断左孩子、右孩子的存在情况:利用数组所以是否越界来判断
  • 比较孩子与双亲的大小。孩子大于双亲,则交换2者,同时以孩子为子堆再执行一次调整。每次交换必须紧跟一次子堆调整。

7、计数排序

计数排序的基本思想:对每一个元素x,确定小于x的元素个数。利用“个数”这一信息,就可以直接把x放到输出排序数组的正确位置上了。
结合直方图理解更加形象。

优点:时间复杂度O(n)

缺点:空间复杂度O(n)+O(N),只能处理非负整数
若事先不了解序列的分布情况
1.可能因为分布过于离散导致空间浪费
2.可能因为分布过于密集,大量重复元素,导致临时空间不足而排序出错

class Solution:
    def sortArray(self, s):
    	if len(s) < 2:
            return s
        # 找到最大最小值
        min_num = min(s)
        max_num = max(s)
        # 计数列表,-min_num可以节省一点空间
        count_list = [0]*(max_num  -min_num + 1)
        # 计数
        for i in s:
            count_list[i - min_num] += 1
        print(count_list)
        res = []
        # 填回。
        for index, count in enumerate(count_list):
            print(index, count)
            while count != 0:
                # 填入的数+min_num,对应于计数阶段的-min_num
                res.append(index + min_num)
                count -= 1
        return res

8、桶排序

桶排序:均匀分布
1.优点:

  • 是对计数排序的改进,计数排序申请的额外空间跨度从最小元素值到最大元素值,若待排序集合中元素不是依次递增的,则必然有空间浪费情况。
  • 桶排序则是弱化了这种浪费情况,将 ** “最小值到最大值之间的每一个位置申请空间” **,更新为 ** “最小值到最大值之间每一个固定区域申请空间” **,尽量减少了元素值大小不连续情况下的空间浪费情况。

2.缺点:

  • 1.需要事先知晓序列分布情况,才能确定一个高效的映射函数
  • 2.对于max范围大的序列需要更多的空间

基本思想:

  • 1.根据待排序集合中最大元素和最小元素的差值范围和映射规则,确定申请的桶个数;
  • 2.遍历待排序集合,将每一个元素移动到对应的桶中(此映射可以将数据初步排序);
  • 3.对每一个桶中元素进行排序,并移动到初始集合中。
  • 4.桶越多则桶内分到的数据越少,排序越快。
class Solution:
    def sortArray(self, s):
        if len(s) < 2:
            return s
        min_num = min(s)
        max_num = max(s)
        # 桶的大小,+1是向上取整
        bucket_range = (max_num - min_num) // len(s) + 1
        # 桶数组
        count_list = [ [] for i in range(len(s))]
        # 向桶数组填数
        for i in s:
        # 映射函数:int((i - min_num) // bucket_range)
            count_list[int((i - min_num) // bucket_range)].append(i)
        s = list([])
        # 回填,这里桶内部排序直接调用了sorted
        for i in count_list:
            for j in sorted(i):
                s.append(j)
        return s

9、基数排序

  • 基数排序的基本思想:按-个-十-百-千位的顺序排序
  • 每一轮排序都是根据序列中的某一位数进行排序,**从低位到高位(!!!)**各进行一次这种排序思想,最终序列便是有序的。
  • 由于输入序列每一位数都是有限的,比如十进制序列,每位数都是0~9的数字,于是可以选择计数排序对序列某一位数进行排序。
  • 同样,基数排序也不能处理非负数。
    1.弥补了计数排序的2个缺点,不论分布离散还是密集都可以
    2.只能处理非负数
class Solution:
    def sortArray(self, s):
        """基数排序"""
        i = 0 # 记录当前正在排拿一位,最低位为1
        max_num = max(s)  # 最大值
        j = len(str(max_num))  # 记录最大值的位数
        while i < j:
            bucket_list =[[] for _ in range(10)] #每趟循环都要初始化桶数组
            for x in s:
                bucket_list[int(x / (10**i)) % 10].append(x) # 找到位置放入桶数组
            s = list([])
            for x in bucket_list:   # 从桶中取出放回原序列
                for y in x:
                    s.append(y)
            i += 1
        return s

例,输入:[90,21,20,30,14,13,15,55,25,43]**

第一趟排序:按个位数排序
[90, 20, 30, 21, 13, 43, 14, 15, 55, 25]

第二趟排序:按十位数排序
[13, 14, 15, 20, 21, 25, 30, 43, 55, 90]

10、排序算法的稳定性

  • 稳定的:如果存在多个具有相同排序码的记录,经过排序后,这些记录的相对次序仍然保持不变,则这种排序算法称为稳定的。

归并排序、插入排序、冒泡排序、桶排序、基数排序、计数排序,都是稳定的排序算法。

  • 否则称为不稳定的。

快速排序、堆排序、shell排序、选择排序,都是不稳定的排序算法。

11、如何选择排序算法?
(1)、数组规模大小

  • 若n较小,使用插入排序、冒泡排序,因为算法原理简单。
  • 若n较大,则应采用时间复杂度较小的排序方法:快速排序(nlogn)、堆排序(nlogn)或归并排序(nlogn)。一般考虑快速排序。
  • 还有线性时间的排序算法,但是限制条件更多。

(2)、要求算法稳定

  • 归并排序、插入排序、冒泡排序、桶排序、基数排序、计数排序

(3)、要求时间稳定

  • 程序无法承受最差排序时间带来的后果时可以选择对排序。堆排序的最大特点就是时间稳定,best与worst都是O(nlogn)

(4)、要求空间消耗少

  • 排除计数排序、基数排序、桶排序。这3种排序算法都是以空间换取时间。一般考虑快速排序。

(5)、数组大量重复

  • 使用三向切分法的改进快速排序。

(6)、数组基本有序

  • 使用随机快速排序、插入排序、冒泡排序。

(7)、数组全为非0数

  • 若对空间复杂度要求不高,可使用桶排序、基数排序、计数排序。

(8)、数组是流形式输入

  • 插入排序、堆排序。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值