斜堆

斜堆(skew heap)是左倾堆的一个变种,与左倾堆一样,合并的时间复杂度也是O(lgn)。相对于左倾堆来说,斜堆没有零距离这个概念,而是在合并操作的时候,每次交换左右子树。

斜堆的定义:

  • 左子树
  • 右子树
  • 键值

合并操作图解

与左倾堆相同,合并操作也是斜堆的重点。合并两个斜堆的过程如下:

  • 如果一个空斜堆与非空斜堆合并,返回非空斜堆;
  • 如果两个斜都非空,比较两个根节点,取优先级高的根节点为新的根节点。将“优先级较高堆的根节点的右孩子”和“优先级底的堆”进行合并;
  • 合并后,交换新堆根节点的左孩子和右孩子;

需要合并的两个堆如下:

第一步 堆 5 大于 堆 9堆 5 作为新堆,堆 5 的右孩子和 堆 9 比较,结果如下:

第二步 堆 9 的右孩子和 堆 19 比较,结果如下:

第三步 堆 15 的右孩子和 堆 19 比较,结果如下:

第四步 堆 19 的右孩子和 堆 25 比较,结果如下:

至此,两颗树合并完成!下面要进行结点交换:

第五步 堆 19 的左、右孩子交换,结果如下:

第六步 堆 15 的左、右孩子交换,结果如下:

第七步 堆 9 的左、右孩子交换,结果如下:

最后一步 堆 5 的左、右孩子交换,结果如下:

至此,两个堆合并完成!

最后,Demo 在这里

左倾堆

堆(heap)被为优先队列(priority queue),但并不是队列按先后顺序来取出元素,而是按照元素的优先级。上一节的 索引堆二叉堆 也是。

这一节我们来介绍左倾堆

左倾堆的介绍

左偏树(英语:leftist tree或leftist heap),也可称为左偏堆、左倾堆,是计算机科学中的一种树,是一种优先队列实现方式 ━━> 维基百科

当优先队列需要解决“两个优先队列合并”的问题时,二叉堆的效率就比较差了,这时左倾堆等方式可以很好的解决这类问题。左倾堆合并时间复杂度为O(lgn)。左倾堆在统计问题,最值问题,模拟问题和贪心问题等问题中有广泛的应用。 左倾堆是二叉树,另外还有两个属性,键值和零距离。 ━━> 来自这里

不同于斜堆合并的平均情况复杂度,左偏堆的合并操作的最坏情况复杂度为O(log n),而完全二叉堆为O(n),所以左偏堆适合基于合并操作的情形。

由于左偏堆已经不是完全二叉树,因此不能用数组存储表示,需要用链接结构。

  • 左子树
  • 右子树
  • 键值
  • NPL

键值

用来比较节点的大小

零距离( NPL )

是 null path length 的缩写,指的是从该结点到达一个没有两个孩子的结点(一个或无孩子)的最短距离,叶节点的NPL为0, NULL的NPL为-1。

左倾堆的性质

  • 节点的优先级大于或等于它子节点的优先级
  • 任意节点左孩子的NPL >= 右孩子的NPL
  • 节点的NPL = 它的右孩子的NPL + 1

下图是 NPL 的示意图:

合并操作图解

合并操作是左倾堆的重点。合并两个左倾堆的过程如下:

  1. 如果一个空左倾堆与非空左倾堆合并,返回非空左倾堆;
  2. 如果两个左倾堆都非空,比较两个根节点,取优先级高的根节点为新的根节点。将“优先级较高堆的根节点的右孩子”和“优先级底的堆”进行合并;
  3. 如果新堆的右孩子的NPL > 左孩子的NPL,则交换左右孩子;
  4. 设置新堆的根节点的NPL = 右子堆NPL + 1;

下图将是我们合并的两个左倾堆:

第一步 将 较小堆(根为10)的右孩子较大堆(根为12) 进行合并

因为 根为 10 的堆根为 12 的堆 优先级高,根为 10 的根节点 作为新节点

第二步 将上一步得到的 根 12 的右子树根为 15 的树 进行合并

第三步 将上一步得到的 根 15 的右子树根为 22 的树 进行合并

第四步 将上一步得到的 根 22 的右子树根为 27 的树 进行合并

至此,已经成功的将两棵树合并成为一棵树了。接下来,对新生成的树进行调节。

第五步 上一步得到的 树22的右孩子的NPL > 左孩子的NPL ,因此交换左右孩子

第六步 上一步得到的 树13的右孩子的NPL > 左孩子的NPL ,因此交换左右孩子

第七步 上一步得到的 树10的右孩子的NPL > 左孩子的NPL ,因此交换左右孩子

至此,合并完毕。上面就是合并得到的左倾堆!

最后,Demo 在这里

参考

纸上谈兵: 左倾堆 (leftist heap)
左倾堆的C语言实现

索引堆

索引堆的原理与实现

索引堆实际上是对二叉堆的优化。

在原来的数据结构中为每个数据增加一个索引,这样在数据更新的时候,只交换其索引,这样对于复杂的数据结构能提高一些性能。如下图:

当索引 10 对应的值改为 70时,进行堆维护之后,则变为:

可以看到,我们仅仅只是对 index 进行了交换,data 的顺序不变。其它的思路不变。

可以把在上节实现的堆排序的代码拿过来修改一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void index_max_heap_shift_up(Index_max_heap *index_max_heap,int k) {
while (k > 1 && index_max_heap->item[index_max_heap->indexs[k/2]] < index_max_heap->item[index_max_heap->indexs[k]]) {
/// 仅交换 index
swap(&index_max_heap->indexs[k/2], &index_max_heap->indexs[k]);
k /= 2;
}
}

static void index_max_heap_shift_down(Index_max_heap *index_max_heap,int k) {
while (2*k <= index_max_heap->count) {
int index = 2*k;
if (index + 1 <= index_max_heap->count && index_max_heap->item[index_max_heap->indexs[index + 1]] > index_max_heap->item[index_max_heap->indexs[index]]) {
index += 1;
}
if (index_max_heap->item[index_max_heap->indexs[k]] > index_max_heap->item[index_max_heap->indexs[index]]) {
break;
}

swap(&index_max_heap->indexs[k], &index_max_heap->indexs[index]);
k = index;
}
}

实现更改索引堆的数据

当数据变化的时候,我们要更改索引堆中对应的数据。假设 indexs 中存储的是数据的索引,现在我们要更改 索引为 5 对应的数据:

1
item[5] = newItem;

现在我们要维护索引堆,但是并不知道 5indexs 中的哪个位置,所以只能循环遍历出 i

1
2
3
4
5
6
7
for (int i = 1; i <= count; i++) {
if (indexs[i] == 5) {
shiftUp(i);
shiftDown(i);
return;
}
}

增加反向查找索引 index

可以看到上面的查找还是比较耗时的,这里我们做一下优化,增加一个反向查找 index 的数组 reverses

我们再将索引 10 对应的值改为 70

全部的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#include "IndexMaxHeap.h"
#include "SortHelper.h"
#include <stdlib.h>
#include <assert.h>


typedef struct Index_max_heap_s Index_max_heap;

typedef struct Index_max_heap_s {
int count; //-> 当前堆内数据的大小
int capacity; //-> 当前堆的容量
void (*insert)(Index_max_heap *,int,int); //-> 插入函数
int (*extract_max)(Index_max_heap *); //-> 输出一个最大值
int (*extract_max_index)(Index_max_heap *); //-> 输出一个最大值的索引
int (*get_item_by_index)(Index_max_heap *,int); //-> 返回一个指定的索引值
void (*change_item_by_index)(Index_max_heap *,int,int); //-> 更改指定索引的值
int *item; //-> 数组指针
int *indexs; //-> 数组指针 索引
int *reverses; //-> 数组指针 反向查找索引
void (*print_to_dot)(Index_max_heap *); //-> 生成 dot 语言 函数
} Index_max_heap;


static void index_max_heap_shift_up(Index_max_heap *index_max_heap,int k) {
while (k > 1 && index_max_heap->item[index_max_heap->indexs[k/2]] < index_max_heap->item[index_max_heap->indexs[k]]) {
/// 仅交换 index
swap(&index_max_heap->indexs[k/2], &index_max_heap->indexs[k]);
index_max_heap->reverses[index_max_heap->indexs[k/2]] = k/2;
index_max_heap->reverses[index_max_heap->indexs[k]] = k;
k /= 2;
}
}

static void index_max_heap_shift_down(Index_max_heap *index_max_heap,int k) {
while (2*k <= index_max_heap->count) {
int index = 2*k;
if (index + 1 <= index_max_heap->count && index_max_heap->item[index_max_heap->indexs[index + 1]] > index_max_heap->item[index_max_heap->indexs[index]]) {
index += 1;
}
if (index_max_heap->item[index_max_heap->indexs[k]] > index_max_heap->item[index_max_heap->indexs[index]]) {
break;
}

swap(&index_max_heap->indexs[k], &index_max_heap->indexs[index]);
index_max_heap->reverses[index_max_heap->indexs[k]] = k;
index_max_heap->reverses[index_max_heap->indexs[index]] = index;

k = index;
}
}

static void insert(Index_max_heap* index_max_heap,int index,int item) {
if (index_max_heap->count > index_max_heap->capacity) {
return;
}
index_max_heap->count += 1;
index_max_heap->item[index_max_heap->count] = item;
index_max_heap->reverses[index] = index_max_heap->count;
index_max_heap->indexs[index_max_heap->count] = index;
index_max_heap_shift_up(index_max_heap, index_max_heap->count);
}

static int extract_max(Index_max_heap* index_max_heap) {
assert(index_max_heap->count > 0);
int ret = index_max_heap->item[index_max_heap->indexs[1]];

swap(&index_max_heap->indexs[1], &index_max_heap->indexs[index_max_heap->count]);
index_max_heap->reverses[index_max_heap->indexs[1]] = 1;
index_max_heap->reverses[index_max_heap->indexs[index_max_heap->count]] = 0;

index_max_heap->count -= 1;

index_max_heap_shift_down(index_max_heap, 1);

return ret;
}

static int extract_max_index(Index_max_heap* index_max_heap) {
assert(index_max_heap->count > 0);
int ret = index_max_heap->indexs[1];

swap(&index_max_heap->indexs[1], &index_max_heap->indexs[index_max_heap->count]);
index_max_heap->reverses[index_max_heap->indexs[1]] = 1;
index_max_heap->reverses[index_max_heap->indexs[index_max_heap->count]] = 0;

index_max_heap->count -= 1;

index_max_heap_shift_down(index_max_heap, 1);

return ret;
}

static int contain(Index_max_heap* index_max_heap,int i) {
assert(i + 1 >= 1 && i + 1 <= index_max_heap->capacity );
return index_max_heap->reverses[i + 1] != 0;
}

static int get_item_by_index(Index_max_heap* index_max_heap,int index) {
assert(contain(index_max_heap,index));
return index_max_heap->item[index + 1];
}

static void change_item_by_index(Index_max_heap* index_max_heap,int index,int item) {
assert(contain(index_max_heap,index));
index += 1;
index_max_heap->item[index] = item;

// for (int i = 1; i <= index_max_heap->count; i++) {
// if (index_max_heap->indexs[i] == index) {
// index_max_heap_shift_up(index_max_heap, i);
// index_max_heap_shift_down(index_max_heap, i);
// return;
// }
// }

int i = index_max_heap->reverses[index];
index_max_heap_shift_up(index_max_heap, i);
index_max_heap_shift_down(index_max_heap, i);
}

static void print_to_dot(Index_max_heap *index_max_heap) {
printf("\n<---------- dot start ---------->\n\n");
printf("digraph heap {\nnode[shape=circle];\nnodesep=0.3;\n");
int count = index_max_heap->count;
for (int i = 1; i <= count; i++) {
int leftIndex = 2 * i;
int rightIndex = 2 * i + 1;
if (leftIndex <= count) {
printf("\t%d -> %d;\n",index_max_heap->item[index_max_heap->indexs[i]],index_max_heap->item[index_max_heap->indexs[leftIndex]]);
}
if (rightIndex <= count) {
printf("\t%d -> %d;\n",index_max_heap->item[index_max_heap->indexs[i]],index_max_heap->item[index_max_heap->indexs[rightIndex]]);
}
}
printf("}\n");
printf("\n\n<---------- dot end ---------->\n");
}


static Index_max_heap create_index_max_heap(int capacity) {

int *item = (int *)malloc(sizeof(int) * (capacity + 1));
int *indexs = (int *)malloc(sizeof(int) * (capacity + 1));
int *reverses = (int *)malloc(sizeof(int) * (capacity + 1));
for (int i = 0; i < capacity; i++) {
item[i] = 0;
indexs[i] = 0;
reverses[i] = 0;
}
Index_max_heap index_max_heap = {
.count = 0,
.capacity = capacity,
.insert = insert,
.extract_max = extract_max,
.extract_max_index = extract_max_index,
.get_item_by_index = get_item_by_index,
.change_item_by_index = change_item_by_index,
.item = item,
.indexs = indexs,
.reverses = reverses,
.print_to_dot = print_to_dot
};
return index_max_heap;
}


#pragma mark - test
void test_index_max_heap() {

// int *a = generateNoRepeatRandomArray(20,0,100);

int arr[] = { 31,2, 1,45,73, 69, 60, 59,91, 49, 43, 32, 29, 18,88, 17, 9,76, 6, 0};

Index_max_heap index_max_heap = create_index_max_heap(50);

for (int i = 0; i < 20; i++) {
index_max_heap.insert(&index_max_heap,i,arr[i]);
}

void (*change_item_by_index)(Index_max_heap *index_max_heap,int,int) = index_max_heap.change_item_by_index;
change_item_by_index(&index_max_heap,8,22);

void (*print_to_dot)(Index_max_heap *index_max_heap) = index_max_heap.print_to_dot;
print_to_dot(&index_max_heap);

int (*extract_max)(Index_max_heap *index_max_heap) = index_max_heap.extract_max;
for (int i = 1; i <= 20; i ++) {
printf("%d, ",extract_max(&index_max_heap));
}
}

关于二叉树的一些概念

一些基本概念

堆的定义

堆(heap),是数据结构中的堆(不是内存模型中的堆)。堆通常是一个可以被看做一棵树,它满足下列性质:

  • 堆中任意节点的值总是不大于(不小于)其子节点的值;
  • 堆总是一棵完全树;

二叉堆的定义

二叉堆是一颗完全树,它分为两种:最大堆和最小堆。

  • 最大堆:父结点的键值总是大于或等于任何一个子节点的键值
  • 最小堆:父结点的键值总是小于或等于任何一个子节点的键值

二叉堆的性质

二叉堆一般都通过”数组”来实现。数组实现的二叉堆,父节点和子节点的位置存在一定的关系。

设二叉堆的顶点索引为 1:

  • 性质一:索引为 i 的左孩子的索引是 2*i
  • 性质二:索引为 i 的右孩子的索引是 2*i + 1
  • 性质三:索引为 i 的父节点的索引是 floor(i/2)

设二叉堆的顶点索引为 0:

  • 性质一:索引为 i 的左孩子的索引是 2*i + 1
  • 性质二:索引为 i 的右孩子的索引是 2*i + 2
  • 性质三:索引为 i 的父节点的索引是 floor((i-1)/2)

二叉堆的基本实现 (最大堆)

用数组存储二叉堆

shiftUp

向二叉堆中添加一个元素,我们就要为它找到一个合适的位置,例如添加的是 95 这个元素

显然此时这个二叉堆已经不满足我们对最大堆的定义,我们要维护的它(最大堆的定义),这里需要几步。我们要不断的把它(95)和它的父节点相比较,大于父节点就交换,直至小于它的父节点。

第一步 和父节点 50 比较

95 大于 50,所以 95 和 父节点 50 交换位置

第二步 和父节点 58 比较

95 大于 58,所以 95 和 父节点 58 交换位置

第三步 和父节点 86 比较

95 大于 86,所以 95 和 父节点 86 交换位置

至此,就完成了整个 shiftUp 的过程

shiftUp 程序的编写

我们首先建立一个结构体:

1
2
3
4
5
6
7
8
9
10
typedef struct Heap_sort_max_s Heap_sort_max;

typedef struct Heap_sort_max_s {
int count; //-> 当前堆内数据的大小
int capacity; //-> 当前堆的容量
void (*insert)(Heap_sort_max *,int); //-> 插入函数
int (*extract_max)(Heap_sort_max *); //-> 输出一个最大值
int *item; //-> 数组指针
void (*print_to_dot)(Heap_sort_max *); //-> 生成 dot 语言 函数
} Heap_sort_max;

insert 函数:

1
2
3
4
5
6
7
8
static void insert(Heap_sort_max* heap_sort_max,int item) {
if (heap_sort_max->count > heap_sort_max->capacity) {
return;
}
heap_sort_max->count += 1;
heap_sort_max->item[heap_sort_max->count] = item;
heap_sort_max_shift_up(heap_sort_max, heap_sort_max->count);
}

shiftUp 函数:

1
2
3
4
5
6
static void heap_sort_max_shift_up(Heap_sort_max *heap_sort_max,int k) {
while (k > 1 && heap_sort_max->item[k/2] < heap_sort_max->item[k]) {
swap(&heap_sort_max->item[k/2], &heap_sort_max->item[k]);
k /= 2;
}
}

其它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static void print_to_dot(Heap_sort_max *heap_sort_max) {
printf("\n<---------- dot start ---------->\n\n");
printf("digraph heap {\nnode[shape=circle];\nnodesep=0.3;\n");
int count = heap_sort_max->count;
for (int i = 1; i <= count; i++) {
int leftIndex = 2 * i;
int rightIndex = 2 * i + 1;
if (leftIndex <= count) {
printf("\t%d -> %d;\n",heap_sort_max->item[i],heap_sort_max->item[leftIndex]);
}
if (rightIndex <= count) {
printf("\t%d -> %d;\n",heap_sort_max->item[i],heap_sort_max->item[rightIndex]);
}
}
printf("}\n");
printf("\n\n<---------- dot end ---------->\n");
}

static Heap_sort_max create_heap_sort_max(int capacity) {

int *p = (int *)malloc(sizeof(int) * capacity);
for (int i = 0; i < capacity; i++) {
p[i] = 0;
}
Heap_sort_max heap_sort_max = {
.count = 0,
.capacity = capacity,
.insert = insert,
.extract_max = extract_max,
.item = p,
.print_to_dot = print_to_dot
};
return heap_sort_max;
}

static void free_heap_sort_max(Heap_sort_max *heap_sort_max) {
free(heap_sort_max->item);
}

测试 insert

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void test_heap_sort_max() {

int *a = generateNoRepeatRandomArray(20,0,100);

Heap_sort_max heap_sort_max = create_heap_sort_max(50);

for (int i = 0; i < 20; i++) {
heap_sort_max.insert(&heap_sort_max,a[i]);
}

void (*print_to_dot)(Heap_sort_max *heap_sort_max) = heap_sort_max.print_to_dot;
print_to_dot(&heap_sort_max);

free_heap_sort_max(&heap_sort_max);
}

我们创建一个空间为 50 大小的堆,向其中插入 20 个随机的元素。最终由 dot 生成的图片:


shiftDown

下面我们要从堆中取出一个最大的元素 95,为了使这个堆仍然是一个完全二叉树,那么我们将最后一个元素放到堆顶,此时的 count 应将 减去一。

现在这个二叉堆已经不满足我们对最大堆的定义,我们要维护的它,对它进行 shiftDown 操作。

第一步 和 子节点 8660 比较

50 小于 它的两个子节点 8660,但 8660 要大,所以 8650 交换:

第二步 和 子节点 4658 比较

50 小于 它的左节点 58 ,大于右结点 46,所以 5850 交换:

最后

由于 50 大于 35,所以不必交换。

至此,就完成了整个 shiftDown 的过程

shiftDown 程序的编写

shiftDown:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void heap_sort_max_shift_down(Heap_sort_max *heap_sort_max,int k) {
while (2*k <= heap_sort_max->count) {
int index = 2*k;
if (index + 1 <= heap_sort_max->count && heap_sort_max->item[index + 1] > heap_sort_max->item[index]) {
index += 1;
}
if (heap_sort_max->item[k] > heap_sort_max->item[index]) {
break;
}

swap(&heap_sort_max->item[k], &heap_sort_max->item[index]);
k = index;
}
}

输出最大值:

1
2
3
4
5
6
7
8
9
10
11
static int extract_max(Heap_sort_max* heap_sort_max) {
assert(heap_sort_max->count > 0);
int ret = heap_sort_max->item[1];

swap(&heap_sort_max->item[1], &heap_sort_max->item[heap_sort_max->count]);
heap_sort_max->count -= 1;

heap_sort_max_shift_down(heap_sort_max, 1);

return ret;
}

堆排序

我们实现了 shiftUpshiftDown 两个操作,那堆排序也就可以实现了。

实现的基本思路是: 先把元素一个一个插入,然后再一个一个去取堆顶元素。代码如下:

1
2
3
4
5
6
7
8
9
10
11
void heap_sort(int arr[],int n) {
Heap_sort_max heap_sort_max = create_heap_sort_max(n + 1);

for (int i = 0; i < n; i++) {
heap_sort_max.insert(&heap_sort_max,arr[i]);
}

for (int i = n-1; i >=0; i--) {
arr[i] = ((int (*)(Heap_sort_max *heap_sort_max))heap_sort_max.extract_max)(&heap_sort_max);
}
}

测试

我们用 50w 个元素测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
**************有序*************
sortname:插入排序优化 sorttime: 0.003090s
sortname:希尔排序   sorttime: 0.025961s
sortname:快速排序随机 sorttime: 0.067989s
sortname:快速排序双路 sorttime: 0.059503s
sortname:快速排序三路 sorttime: 0.109031s
sortname:堆排序    sorttime: 0.283506s
**************近乎有序*************
sortname:插入排序优化 sorttime: 0.011907s
sortname:希尔排序   sorttime: 0.031257s
sortname:快速排序随机 sorttime: 0.069627s
sortname:快速排序双路 sorttime: 0.047605s
sortname:快速排序三路 sorttime: 0.090930s
sortname:堆排序    sorttime: 0.249643s
**************有大量的重复值*************
sortname:希尔排序   sorttime: 0.055228s
sortname:快速排序随机 sorttime: 27.275784s
sortname:快速排序双路 sorttime: 0.055961s
sortname:快速排序三路 sorttime: 0.015928s
sortname:堆排序    sorttime: 0.151020s
**************随机*************
sortname:希尔排序   sorttime: 0.197971s
sortname:快速排序随机 sorttime: 0.099134s
sortname:快速排序双路 sorttime: 0.103923s
sortname:快速排序三路 sorttime: 0.120655s
sortname:堆排序    sorttime: 0.210723s
**************倒叙*************
sortname:希尔排序   sorttime: 0.057273s
sortname:快速排序随机 sorttime: 0.077192s
sortname:快速排序双路 sorttime: 0.049288s
sortname:快速排序三路 sorttime: 0.092265s
sortname:堆排序    sorttime: 0.130887s

可以看到是,堆排序要比其它的排序稍稍慢一些,不过还是可以接受的。

堆排序优化 heapify

将排序的数组直接赋值给堆,然后进行 shiftDown 操作,省去 insert 操作。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void heapify_sort(int arr[],int n) {
Heap_sort_max heap_sort_max = create_heap_sort_max(n + 1);
for (int i = 0; i < n; i ++) {
heap_sort_max.item[i+1] = arr[i];
}
heap_sort_max.count = n;

for (int i = n/2; i >= 1; i--) {
heap_sort_max_shift_down(&heap_sort_max, i);/// O(n)
}

for (int i = n-1; i >=0; i--) {
arr[i] = ((int (*)(Heap_sort_max *heap_sort_max))heap_sort_max.extract_max)(&heap_sort_max);
}
}

测试

我们用 50w 个元素测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
**************有序*************
sortname:插入排序优化 sorttime: 0.003090s
sortname:希尔排序   sorttime: 0.025961s
sortname:快速排序随机 sorttime: 0.067989s
sortname:快速排序双路 sorttime: 0.059503s
sortname:快速排序三路 sorttime: 0.109031s
sortname:堆排序    sorttime: 0.283506s
sortname:堆排序优化  sorttime: 0.130963s
**************近乎有序*************
sortname:插入排序优化 sorttime: 0.011907s
sortname:希尔排序   sorttime: 0.031257s
sortname:快速排序随机 sorttime: 0.069627s
sortname:快速排序双路 sorttime: 0.047605s
sortname:快速排序三路 sorttime: 0.090930s
sortname:堆排序    sorttime: 0.249643s
sortname:堆排序优化  sorttime: 0.127976s
**************有大量的重复值*************
sortname:希尔排序   sorttime: 0.055228s
sortname:快速排序随机 sorttime: 27.275784s
sortname:快速排序双路 sorttime: 0.055961s
sortname:快速排序三路 sorttime: 0.015928s
sortname:堆排序    sorttime: 0.151020s
sortname:堆排序优化  sorttime: 0.132577s
**************随机*************
sortname:希尔排序   sorttime: 0.197971s
sortname:快速排序随机 sorttime: 0.099134s
sortname:快速排序双路 sorttime: 0.103923s
sortname:快速排序三路 sorttime: 0.120655s
sortname:堆排序    sorttime: 0.210723s
sortname:堆排序优化  sorttime: 0.212289s
**************倒叙*************
sortname:希尔排序   sorttime: 0.057273s
sortname:快速排序随机 sorttime: 0.077192s
sortname:快速排序双路 sorttime: 0.049288s
sortname:快速排序三路 sorttime: 0.092265s
sortname:堆排序    sorttime: 0.130887s
sortname:堆排序优化  sorttime: 0.130506s

可以看到比之前稍稍快一点点。

原地堆排序

之前的堆排序都是用了额外的空,原地堆排序的空间复杂度为 O(1)。

ps:原地堆排序的顶元素是从 0 开始的。

原地堆排序分析

首先二叉堆数组是这样的:

然后不断的将堆顶元素和堆尾的元素交换,并且每次交换后将堆元素的大小减一,当剩下最后一个的时候,数组已经从小到大排好序了。

由于原地堆排序的顶元素是从 0 开始的,我们要重新实现 shiftDown 操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 顶结点的索引从 0 开始
static void heap_autochthonous_sort_shift_down(int arr[],int n,int k) {
while ((2*k + 1) < n) {
int index = 2*k + 1; // 取左孩子索引
if (index + 1 < n && arr[index + 1] > arr[index]) { index++; }
if (arr[k] > arr[index]) { break; }
swap(&arr[k], &arr[index]);
k = index;
}
}

/// 原地堆排序 空间O(1)
void heap_autochthonous_sort(int arr[],int n) {
/// heapify
for (int i = (n-1)/2; i >= 0; i--) {
heap_autochthonous_sort_shift_down(arr, n, i);
}

for (int i = n-1; i > 0; i--) {
swap(&arr[0], &arr[i]);
heap_autochthonous_sort_shift_down(arr, i, 0);
}
}

测试

我们用 50w 个元素测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
**************有序*************
sortname:插入排序优化 sorttime: 0.003090s
sortname:希尔排序   sorttime: 0.025961s
sortname:快速排序随机 sorttime: 0.067989s
sortname:快速排序双路 sorttime: 0.059503s
sortname:快速排序三路 sorttime: 0.109031s
sortname:堆排序    sorttime: 0.283506s
sortname:堆排序优化  sorttime: 0.130963s
sortname:堆排序原地  sorttime: 0.113156s
**************近乎有序*************
sortname:插入排序优化 sorttime: 0.011907s
sortname:希尔排序   sorttime: 0.031257s
sortname:快速排序随机 sorttime: 0.069627s
sortname:快速排序双路 sorttime: 0.047605s
sortname:快速排序三路 sorttime: 0.090930s
sortname:堆排序    sorttime: 0.249643s
sortname:堆排序优化  sorttime: 0.127976s
sortname:堆排序原地  sorttime: 0.107415s

**************有大量的重复值*************
sortname:希尔排序   sorttime: 0.055228s
sortname:快速排序随机 sorttime: 27.275784s
sortname:快速排序双路 sorttime: 0.055961s
sortname:快速排序三路 sorttime: 0.015928s
sortname:堆排序    sorttime: 0.151020s
sortname:堆排序优化  sorttime: 0.132577s
sortname:堆排序原地  sorttime: 0.104522s

**************随机*************
sortname:希尔排序   sorttime: 0.197971s
sortname:快速排序随机 sorttime: 0.099134s
sortname:快速排序双路 sorttime: 0.103923s
sortname:快速排序三路 sorttime: 0.120655s
sortname:堆排序    sorttime: 0.210723s
sortname:堆排序优化  sorttime: 0.212289s
sortname:堆排序原地  sorttime: 0.180132s

**************逆序*************
sortname:希尔排序   sorttime: 0.057273s
sortname:快速排序随机 sorttime: 0.077192s
sortname:快速排序双路 sorttime: 0.049288s
sortname:快速排序三路 sorttime: 0.092265s
sortname:堆排序    sorttime: 0.130887s
sortname:堆排序优化  sorttime: 0.130506s
sortname:堆排序原地  sorttime: 0.104213s

可以看到又稍稍快了一点。

最后附上Demo

结束

至此,整个排序算法便介绍完了。

下面是各个排序的时间复杂度和空间复杂度的对比:

平均时间复杂度 原地排序 额外空间 稳定排序
插入排序 O(n^2 ) O(1)
归并排序 O(nlogn) O(n)
快速排序 O(nlogn) O(logn)
堆排序 O(nlogn) O(1)

排序算法之快速排序及其优化

快速排序介绍

快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),一种排序算法,最早由东尼·霍尔提出。使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。

快速排序通常明显比其他Ο(n log n)算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。在平均状况下,排序n个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n\^2)次比较,但这种状况并不常见。

快速排序 看图说话

实例分析排序过程

待排数组:

快速排序01

选取数组的第一个元素为基准:5

将小于5的元素放在其左边,大于5的元素放在其右边:

快速排序02

然后不断的对左右两边进行上面的分割,最终为:

快速排序04

注意:我用线将当前的数组的顺序连接了起来,这已经是排序完成了,大家看了下面的分割过程,自己将这个过程用这样的图在纸上或者keynote等画出来,之后,下面的程序是非常好理解的!

分析分割过程

我们来看分割: 定义 v 为基准值;定义 l 为数组的第一个元素的索引;j 为小于基准值的最后一个元素的索引;i 为当前和基准值比较的值的索引,e 为 其值。

数组arr中 [l+1,j] 区间的值都是 小于 v 的,[j+1,i-1] 都是大于 v 的,具体如下图:

快速排序05

1.当 e 小于 v 的时候:
j+1 位置的值 和 e 交换:

快速排序06

然后 j ++,i ++,就行了;

2.当 e 大于 v 的时候:

这个就简单了 直接 i ++ 就 O 了;

3.当分割完了之后:

快速排序07

此时,只要将 l 和 j 的位置的值交换一下:

快速排序08

我们已经将数组分割成为:左边小于 v ,右边 大于 v.

关于边界处理,请看后面的程序。

下面是分割的代码:

1
2
3
4
5
6
7
8
9
10
11
12
int __partition(int arr[],int l,int r) {
int v = arr[l];
int j = l;
for (int i = l+1; i <= r; i++) {
if (arr[i] < v) {
swap(&arr[j +1], &arr[i]);
j ++;
}
}
swap(&arr[l], &arr[j]);
return j;
}

相信,有了前面的图,理解这个程序是很容易的。

快速排序,使用递归的方法是很容易理解的,我们这里也采用递归的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void __quickSort(int arr[],int l,int r) {
if (l >= r) {
return;
}

int p = __partition(arr,l,r);

__quickSort(arr, l, p-1);
__quickSort(arr, p+1, r);
}

void quickSort(int arr[],int n) {
__quickSort(arr,0,n-1);
}

上面不断的递归分割,直到 l > r 结束递归;你能一步步的看到这里,就可以在纸上,手撸快速排序的代码了。

测试一下

和前面的一样 还是四种测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
**************有序*************
sortname:希尔排序   sorttime: 0.004465s
sortname:归并排序    sorttime: 0.023814s
sortname:快速排序   sorttime: 14.126987s

**************近乎有序*************
sortname:希尔排序   sorttime: 0.015746s
sortname:归并排序    sorttime: 0.031117s
sortname:快速排序   sorttime: 0.896174s

**************随机*************
sortname:希尔排序   sorttime: 0.034790s
sortname:归并排序    sorttime: 0.037268s
sortname:快速排序   sorttime: 0.018142s

**************倒叙*************
sortname:希尔排序   sorttime: 0.010014s
sortname:归并排序    sorttime: 0.024656s
sortname:快速排序   sorttime: 23.304824s

快速排序不是号称最快的排序方法么,可是测试结果说明了一切 😂😂😂

其实我们写的这个并没有经过优化,并且快速排序并非能适应所有的排序场景。

随机化快速排序

首先想一下归并排序是不是和快速排序的方法很相似啊,不同的是归并排序是等分的而快速排序不是的,是根据选择的基值来分的,我们上面选择的是数组的第一个

快速排序09

快速排序10

最差的情况是测试用例中 倒叙的:

快速排序10

我们想的是:每次得取数组中中间的那个数值作为基值,但这样做需要重新遍历一遍数组,耗费了大量的时间。我们可以去个折中的方法,就是随机取数组中的值作为基值,且看程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
int __partition_00(int arr[],int l,int r) {
swap(&arr[l], &arr[rand()%(r-l+1) + l]);
int v = arr[l];
int j = l;
for (int i = l+1; i <= r; i++) {
if (arr[i] < v) {
swap(&arr[j +1], &arr[i]);
j ++;
}
}
swap(&arr[l], &arr[j]);
return j;
}

程序中的 swap(&arr[l], &arr[rand()%(r-l+1) + l]); 就是使用 rand() 函数 随机取了值 和 为l索引上的值交换,作为基值,虽然还是有可能 降为 n^2,但概率远远的降低了。

我们再来看测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
**************有序*************
sortname:希尔排序   sorttime: 0.004857s
sortname:归并排序   sorttime: 0.026325s
sortname:快速排序   sorttime: 14.352925s
sortname:快速排序随机 sorttime: 0.013611s

**************近乎有序*************
sortname:希尔排序   sorttime: 0.012678s
sortname:归并排序   sorttime: 0.025845s
sortname:快速排序   sorttime: 0.632769s
sortname:快速排序随机 sorttime: 0.013047s

**************随机*************
sortname:希尔排序   sorttime: 0.031845s
sortname:归并排序   sorttime: 0.036922s
sortname:快速排序   sorttime: 0.016712s
sortname:快速排序随机 sorttime: 0.020723s

**************倒序*************
sortname:希尔排序   sorttime: 0.010177s
sortname:归并排序   sorttime: 0.023441s
sortname:快速排序   sorttime: 23.705080s
sortname:快速排序随机 sorttime: 0.014164s

我们再看时间,是不是在性能上有不小的提升


双路快速排序

在上面测试的几种用例当中 有大量的重复值 这一项让快速排序慢的不是一点点。那为什么这么慢呢?

我们来看一组测试数据:

1
5, 6, 5, 5, 5, 5, 8, 3, 5, 5

第一次进行分割之后变成:

1
[3]   [5, 5, 5, 5, 5, 8, 6, 5, 5]

相信看到这里大家就明白了,快速排序变成了 O(n^2 ) 了:

快速排序10

在这里介绍一种解决方法:双路快速排序

双路快速排序的分析

我们用两路分别从左右两边进行分割;

  • l:指向选取的 v 在数组的首位;
  • i:从左向右指向下一个要扫描的元素;
  • j:从右向左指向下一个要扫描的元素;

然后进行如下操作:

  1. 从左向右扫描直到 >= v 停止;
  2. 从右向左扫描直到 <= v 停止;
  3. 交换 i 和 j 指向的元素;
  4. i++,j++;
  5. 直到 j <= i 停止扫描;

大家可能会有这样的一个疑问:当 左右的两个 e 元素 相等时也会进行交换的

是的,这样可以确保在一定程度上分割平衡。所以分割实际上是这样的:

arr[l+1,i-1] <= v 和 arr[j,r] >= v

双路快速排序的程序代码

现在分割函数写成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __partition_twoWays(int arr[],int l,int r) {
swap(&arr[l], &arr[rand()%(r-l+1) + l]);
int v = arr[l];
int i = l + 1;
int j = r;
while (1) {
while (i <= r && arr[i] < v) { i ++; }
while (j >= l+1 && arr[j] > v) { j --; }
if (j <= i) { break; }
swap(&arr[i], &arr[j]);
i++;
j--;
}
swap(&arr[l], &arr[j]);
return j;
}

测试

我们再来看测试(测试用例 50w个):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
**************有序*************
sortname:插入排序优化 sorttime: 0.002745s
sortname:希尔排序   sorttime: 0.026173s
sortname:快速排序随机 sorttime: 0.078291s
sortname:快速排序双路 sorttime: 0.047122s

**************近乎有序*************
sortname:插入排序优化 sorttime: 0.010042s
sortname:希尔排序   sorttime: 0.030206s
sortname:快速排序随机 sorttime: 0.063545s
sortname:快速排序双路 sorttime: 0.046858s

**************有大量的重复值*************
sortname:希尔排序   sorttime: 0.053225s
sortname:快速排序随机 sorttime: 27.211613s
sortname:快速排序双路 sorttime: 0.058205s

**************随机*************
sortname:希尔排序   sorttime: 0.179634s
sortname:快速排序随机 sorttime: 0.102915s
sortname:快速排序双路 sorttime: 0.102822s

**************倒叙*************
sortname:希尔排序   sorttime: 0.056003s
sortname:快速排序随机 sorttime: 0.065304s
sortname:快速排序双路 sorttime: 0.048225s

我们可以看到 双路快速排序 的时间相比之前快了不少!


三路快速排序

关于 有大量的重复值 的快速排序还有一种经典的解法 -> 三路快速排序

三路快速排序的分析

我们重新定义了如下几个变量:

  • l:指向选取的 v 在数组的首位;
  • lt:从左向右 小于v 的最后一个元素(初始情况: lt = l);
  • i:从左向右指向下一个要扫描的元素(初始情况: i = l + 1);
  • gt:从右向左 大于v 的第一个元素(初始情况: gt = r + 1);

初始的情况

当 e == v 时

这个很简单 直接 i ++ 就行了,如下图

然后:

当 e < v 时

i 指向的元素 和 lt+1 指向的元素 交换位置,然后 i++ , lt++

然后:

当 e > v 时

i 指向的元素 和 gt-1 指向的元素 交换位置,然后 gt –

然后:

扫描完成时

三路快速排序的程序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void __quickSort_threeWays(int arr[],int l,int r) {
if (l >= r) {
return;
}
swap(&arr[l], &arr[rand()%(r-l+1) + l]);
int v = arr[l];
// arr[l+1...lt] < v 、 arr[lt+1...i-1] == v 和 arr[gt...r] > v
int lt = l;
int i = l + 1;
int gt = r + 1;
while (i < gt) {
if (arr[i] < v) {
swap(&arr[i], &arr[lt + 1]);
i++;
lt++;
} else if (arr[i] > v) {
swap(&arr[i], &arr[gt - 1]);
gt --;
} else {
i ++;
}
}
swap(&arr[l], &arr[lt]);
__quickSort_threeWays(arr, l, lt-1);
__quickSort_threeWays(arr, gt, r);
}

void quickSort_threeWays(int arr[],int n) {
srand((unsigned)time(NULL));
__quickSort_threeWays(arr,0,n-1);
}

测试

我们再来看测试(测试用例 50w个):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
**************有序*************
sortname:插入排序优化 sorttime: 0.003101s
sortname:希尔排序   sorttime: 0.023716s
sortname:快速排序随机 sorttime: 0.070738s
sortname:快速排序双路 sorttime: 0.057771s
sortname:快速排序三路 sorttime: 0.110716s

**************近乎有序*************
sortname:插入排序优化 sorttime: 0.014255s
sortname:希尔排序   sorttime: 0.033605s
sortname:快速排序随机 sorttime: 0.067371s
sortname:快速排序双路 sorttime: 0.051204s
sortname:快速排序三路 sorttime: 0.106871s

**************有大量的重复值*************
sortname:希尔排序   sorttime: 0.054126s
sortname:快速排序随机 sorttime: 27.016716s
sortname:快速排序双路 sorttime: 0.054265s
sortname:快速排序三路 sorttime: 0.014668s

**************随机*************
sortname:希尔排序   sorttime: 0.175923s
sortname:快速排序随机 sorttime: 0.099126s
sortname:快速排序双路 sorttime: 0.098717s
sortname:快速排序三路 sorttime: 0.111747s

**************倒叙*************
sortname:希尔排序   sorttime: 0.059569s
sortname:快速排序随机 sorttime: 0.065489s
sortname:快速排序双路 sorttime: 0.047557s
sortname:快速排序三路 sorttime: 0.090296s

我们可以看到对 有大量的重复值 排序又快了一点点。当然在其它情况下,三路快速排序的时间都很稳定,所以三路快速排序应当是首选。


结束

至此,快速排序便介绍完了,后面即将介绍 堆排序

最后附上Demo

排序算法之归并排序及其优化

归并排序介绍

归并排序是创建在归并操作上的一种有效的排序算法,效率为O(n log n)。1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。

自顶向下

不说废话了,开工。
待排数组如下:

插入排序初始数组

第一步

我们首先将这 8 个元素,一层一层的分割,如:

  • 第一层两组:[8 , 6, 2 , 3],[ 1, 5, 7, 4]
  • 第二层四组:[8, 6], [2, 3], [1, 5], [7, 4]
  • 第三层八组:[8], [6], [2], [3], [1], [5], [7], [4]

如下图:

归并排序01

第二步 归并操作(Merge)

从底层开始两两归并(当然也可以三三进行归并),归并之后是有序的。

第三层八组,进行两两归并:

归并排序02

第二层四组,进行两两归并:

归并排序03

第一层两组,进行两两归并:

归并排序04

此时,数组已经排序完成!

第三步 分析程序

很明显,上面的是自顶向下,采用的是分治法,我们采用递归的方法,来实现这个算法:

1
2
3
void mergeSort(int arr[],int n) {
__mergeSort(arr, 0, n-1);
}

开始分割:

1
2
3
4
5
6
7
8
9
10
11
12
13
void __mergeSort(int arr[],int l,int r) {

int mid = l + (r - l)/2;

if (l >= r) {
return;
}

__mergeSort(arr, l, mid);
__mergeSort(arr, mid + 1, r);

_merge(arr, l, mid, r);
}

注意:这里取中间的 mid 有个问题,大多数人都会写成这样:

1
int mid = (l + r)/2;

这样写不严谨,当lr 都很大时,(l + r) 会溢出

先不断的分割[l,mid],然后[mid+1,r]

最后归并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void _merge(int arr[],int l, int mid,int r) {
int count = r-l+1;
int ta[count];

for (int i = l; i <= r; i++) {
int a = i - l;
ta[a] = arr[i];
}

int i = l,j = mid + 1;
for (int k = l; k <= r; k++) {
if (i > mid) {
arr[k] = ta[j - l];
j ++;
} else if (j > r) {
arr[k] = ta[i - l];
i ++ ;
} else if (ta[i - l] < ta[j - l]) {
arr[k] = ta[i-l];
i ++;
} else {
arr[k] = ta[j - l];
j ++;
}
}
}

这里创建了一个临时的数组长度为 (r - l) + 1 .

注意:如果数据量很大,可能会造成栈溢出,这是归并排序的一个缺点。

咱们来分析一下归并排序的最后一步:

归并排序05

先排除边界:

  • i > mid 时,说明[l,mid]区间已经结束,所以 [mid+1,r] 直接赋值就行了;
  • j > r 时,说明[mid+1,r]区间已经结束,所以 [l,mid] 直接赋值就行了;

开始比较:

第一次比较

i -> 2,j -> 1; 2 > 1,将 1 放进去:

归并排序06

第二次比较

此时数组的状态及各个变量的指向:

归并排序07

i -> 2,j -> 4; 2 < 4,将 2 放进去:

归并排序08

第三次比较

此时数组的状态及各个变量的指向:

归并排序09

i -> 3,j -> 4; 3 < 4,将 3 放进去:

归并排序11

第四次比较

此时数组的状态及各个变量的指向:

归并排序10

i -> 6,j -> 4; 6 > 4,将 4 放进去:

归并排序12

后面的就不一一解释了,按照这个思路,请继续往下画…

我相信前面的程序,现在是非常清晰了。

自低向上

其实自低向上很好理解,就是第一步被分割后,开始向上归并,只不过不是使用递归的解法,而是迭代法。

我这里把程序贴出来,大家跟着程序把图画一下,就理解了。

1
2
3
4
5
6
7
8
void mergeSortBackUp(int arr[],int n) {

for (int size = 1; size <= n; size += size ) {
for (int i = 0; i + size < n; i += (size + size)) {
_merge(arr, i, i + size - 1, MIN(i + size + size - 1,n-1));
}
}
}

注意:这里的_merge 函数 用的是上一步的!

测试

这里用了 1千万一个数据测试,测试结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
**************有序*************
sortname:归并排序    sorttime: 3.218722s
sortname:归并排序2   sorttime: 2.599350s

**************近乎有序*************
sortname:归并排序    sorttime: 3.078164s
sortname:归并排序2   sorttime: 2.605246s

**************随机*************
sortname:归并排序    sorttime: 4.810648s
sortname:归并排序2   sorttime: 4.118894s

**************倒叙*************
sortname:归并排序    sorttime: 3.054663s
sortname:归并排序2   sorttime: 2.569524s

最后附上Demo

前言

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。

希尔排序的实质就是分组插入排序,它们的思想高度是一致的的(为了共产主义😁😁😁),不过希尔排序的速度蛮快的,甚至可以与快速排序相媲美!

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  • 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;
  • 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

不好意思上面的部分来自维基百科

好了,第一部分介绍完了,我们来看图说画:


看图说话

以一个 n = 11 的数组为例:

希尔排序01

第一次

第一步 设步长为 2 则 int gap = 11/2 = 5

将数组分为 5 组,每组两个数据

希尔排序02

第二步

那么就被分成了 A B C D E 这几组数据:

希尔排序03

第三步

对每组数据进行排序:

希尔排序04

于是数组变为下面的:

希尔排序05

第二次

第一步 重复上面第一步 int gap = 5/2 = 2

将数组分为 2 组,每组5个数据

希尔排序06

第二步

那么就被分成了 A B 这两组数据:

希尔排序07

第三步

对每组数据进行排序:

希尔排序08

于是数组变为下面的:

希尔排序09

第三次

第一步 重复上面第一步 int gap = 2/2 = 1

将数组分为 1 组,那就是整个数组

希尔排序09

对整个数组进行排序:
希尔排序10

第四次

第一步 重复上面第一步 int gap = 1/2 = 0

则此时 数组排序已经完成


看程序

经过上面的一步步的分析,相信大家已经可以看得懂下面的程序(按照上面的理解即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void shellsort2(int a[], int n) {

int j, gap;

for (gap = n / 2; gap > 0; gap /= 2)
for (j = gap; j < n; j++)//从数组第gap个元素开始
if (a[j] < a[j - gap])//每个元素与自己组内的数据进行直接插入排序
{
int temp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > temp)
{
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = temp;
}
}

测试

下面我们开始测试,同样的四种:

  1. 有序
  2. 近乎有序
  3. 随机
  4. 倒叙

我们测试100W个数据,测试它的排序时间,测试用例的程序在这里,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int n  = 100000;
// 有序
int *a = generateSequenceArray(n);
// 近乎有序
int *b = generateRandomSequenceArray(n, 100);
// 随机
int *c = generateRandomArray(n,0,n);
// 倒叙
int *d = generateReverseArray(n);

int *a1 = copyGenerateRandomArray(a, n);
int *a2 = copyGenerateRandomArray(a, n);
int *a3 = copyGenerateRandomArray(a, n);

int *b1 = copyGenerateRandomArray(b, n);
int *b2 = copyGenerateRandomArray(b, n);
int *b3 = copyGenerateRandomArray(b, n);

int *c1 = copyGenerateRandomArray(c, n);
int *c2 = copyGenerateRandomArray(c, n);
int *c3 = copyGenerateRandomArray(c, n);

int *d1 = copyGenerateRandomArray(d, n);
int *d2 = copyGenerateRandomArray(d, n);
int *d3 = copyGenerateRandomArray(d, n);

printf("**************有序*************\n");
testSort("插入排序  ", insertSort,a1, n);
testSort("插入排序优化", insertSort2,a2, n);
testSort("希尔排序  ", shellsort2,a3, n);
printf("\n**************近乎有序*************\n");
testSort("插入排序  ", insertSort,b1, n);
testSort("插入排序优化", insertSort2,b2, n);
testSort("希尔排序  ", shellsort2,b3, n);
printf("\n**************随机*************\n");
testSort("插入排序  ", insertSort,c1, n);
testSort("插入排序优化", insertSort2,c2, n);
testSort("希尔排序  ", shellsort2,c3, n);
printf("\n**************倒叙*************\n");
testSort("插入排序  ", insertSort,d1, n);
testSort("插入排序优化", insertSort2,d2, n);
testSort("希尔排序  ", shellsort2,d3, n);

free(a);free(a1);free(a2);free(a3);

free(b);free(b1);free(b2);free(b3);

free(c);free(c1);free(c2);free(c3);

free(d);free(d1);free(d2);free(d3);

log如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
**************有序*************
sortname:插入排序   sorttime: 0.000404s
sortname:插入排序优化 sorttime: 0.000474s
sortname:希尔排序   sorttime: 0.003668s

**************近乎有序*************
sortname:插入排序   sorttime: 0.042827s
sortname:插入排序优化 sorttime: 0.020470s
sortname:希尔排序   sorttime: 0.011824s

**************随机*************
sortname:插入排序   sorttime: 17.607321s
sortname:插入排序优化 sorttime: 8.014994s
sortname:希尔排序   sorttime: 0.032545s

**************倒叙*************
sortname:插入排序   sorttime: 35.988631s
sortname:插入排序优化 sorttime: 16.293709s
sortname:希尔排序   sorttime: 0.009289s

由此可以看出 希尔排序的强悍,特别是对于最坏情况的排序,是未优化过的插入排序的近4000倍!

不知道希尔排序 大家搞明白了么!!! 😀😀😀

最后附上Demo

排序算法之插入排序

从这周开始,每周我将写一些工作中常用的算法如下:

  1. 冒泡排序和选择排序
  2. 插入排序;
  3. 希尔排序;
  4. 归并排序及其优化;
  5. 快速排序及其优化;
  6. 堆排序;

下面开始正文:

插入排序

关于插入排序的理论部分就不说了,去看维基百科,里面有各种语言的实现版本。咱们看图说话,容易理解。

待排数组如下:

插入排序初始数组

第一步

我们首先考虑 8 这个元素,只有 8 这一个元素,所以就排好序了。

插入排序01

第二步

我们来考虑 6 这个元素:

插入排序02

我们要把 6 放在其前面数组中合适的位置,和它前面的 8 相比 68 要小,所以 68 要互换位置:

插入排序03

此时前两个元素就排好的位置:

插入排序04

第三步

我们来考虑 2 这个元素:

插入排序05
我们把 28 比,28 小,所以互换位置:

插入排序06
然后,把 26 比,26 小,所以互换位置:

插入排序07

至此前三个元素就排好的位置:

插入排序08

第四步

我们来考虑 3 这个元素:

插入排序09
我们把 38 比,38 小,所以互换位置:

插入排序10

然后,把 36 比,36 小,所以互换位置:

插入排序11
然后,把 32 比,32 大,所以不互换位置。

插入排序11
至此前四个元素就排好的位置:

插入排序12
后面就不一一列举了,思路尽在 一二三四步骤中。

写程序

上面的思路理解了,下面的程序就比较好写了:

插入排序13
我们从数组的索引为 1 的位置开始,因为索引为 0 的位置已经排好序(假设数组长度为 n)即:

1
2
3
for (int i = 1; i < n; i++) {

}

大范围的确定好了,我们要进行小范围的比较排序了,那么 1位置的元素假设为第 j 位,要和之前的 位置 j-1位置的元素比较,如果大于不需要交换,否则需要交换:那么:

1
2
3
4
5
6
7
for (int j = i; j > 0; j--) {
if (arr[j] < arr[j - 1]) {
swap(&arr[j], &arr[j-1]);
} else {
break;
}
}

故完整的如下:

1
2
3
4
5
6
7
8
9
for (int i = 1; i < n; i++) {
for (int j = i; j > 0; j--) {
if (arr[j] < arr[j - 1]) {
swap(&arr[j], &arr[j-1]);
} else {
break;
}
}
}

当然,也可以这么写(嗯,更好看,装下逼也是可以的😜😜😜):

1
2
3
4
5
6
7
8
9
void insertSort(int arr[],int n) {

for (int i = 1; i < n; i++) {
for (int j = i; j > 0 && (arr[j] < arr[j - 1]); j--) {
swap(&arr[j], &arr[j-1]);

}
}
}

测试

下面我们开始测试。测试用例呢 分为四种,分别为:

  1. 有序
  2. 近乎有序
  3. 随机
  4. 倒叙

我们测试10W个数据,测试它的排序时间,测试用例的程序在这里,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int n  = 100000;
// 有序
int *a = generateSequenceArray(n);
// 近乎有序
int *b = generateRandomSequenceArray(n, 100);
// 随机
int *c = generateRandomArray(n,0,n);
// 倒叙
int *d = generateReverseArray(n);

int *a1 = copyGenerateRandomArray(a, n);

int *b1 = copyGenerateRandomArray(b, n);

int *c1 = copyGenerateRandomArray(c, n);

int *d1 = copyGenerateRandomArray(d, n);

printf("**************有序*************\n");
testSort("插入排序  ", insertSort,a1, n);
printf("\n**************近乎有序*************\n");
testSort("插入排序  ", insertSort,b1, n);
printf("\n**************随机*************\n");
testSort("插入排序  ", insertSort,c1, n);
printf("\n**************倒叙*************\n");
testSort("插入排序  ", insertSort,d1, n);

free(a);free(a1);

free(b);free(b1);

free(c);free(c1);

free(d);free(d1);

运行之后的log(这里是在MacBook Pro上测试的结果,其它可能不是这个时间,但大底相同):

1
2
3
4
5
6
7
8
9
10
11
**************有序*************
sortname:插入排序   sorttime: 0.000647s

**************近乎有序*************
sortname:插入排序   sorttime: 0.051551s

**************随机*************
sortname:插入排序   sorttime: 18.110468s

**************倒叙*************
sortname:插入排序   sorttime: 36.259288s

由此可以看出:插入排序对于近乎有序的数组用时很少,当数组随机或者是最坏情况,那就比较耗时。

那,我们能不能改进呢? 答案是可以的

插入排序的优化

在上面的看图说话中,某个元素要想找到自己的位置,则必须和之前的比较,完了之后可能要交换位置,如果说该元素是最小的,且是最后一个,那要交换 n-1 个了。我们知道 交换元素 是比较耗时的操作,那能不能不交换元素,也能找到它的合适的位置呢?方法还是的有的!采用赋值的方法

我们看第四步:

插入排序09
进行比较之后 3 比 8 小,我们在这里不进行交换,而是将 3 这个值记录下来为 e = 3,然后将 8 元素值 赋值到 3 的位置

插入排序14
然后将 e = 3元素 6 比较,3 比 6 小,将 元素 6 赋值给其后一位:

插入排序15
然后将 e = 3元素 2 比较,3 比 2 大,所以 元素 3 就找到了自己的位置:

插入排序16
看完图之后,这个思路是不是很清晰,下面的程序也是非常好写的:

1
2
3
4
5
6
7
8
9
for (int i = 1; i < n; i++) {

int e = arr[i];//将该元素记录下来
int j;
for (j = i; j > 0 && arr[j - 1] > e; j--) {
arr[j] = arr[j - 1];
}
arr[j] = e;
}

插入排序优化测试

我们利用前面的测试方法,测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
**************有序*************
sortname:插入排序   sorttime: 0.000647s
sortname:插入排序优化 sorttime: 0.000554s

**************近乎有序*************
sortname:插入排序   sorttime: 0.051551s
sortname:插入排序优化 sorttime: 0.025874s

**************随机*************
sortname:插入排序   sorttime: 18.110468s
sortname:插入排序优化 sorttime: 8.335453s

**************倒叙*************
sortname:插入排序   sorttime: 36.259288s
sortname:插入排序优化 sorttime: 16.780035s

看看测试时间,这个优化还是有明显的提高的!!!

不知道这一节你掌握了么?😜😜😜

最后附上Demo

从这周开始,每周我将写一些工作中常用的算法如下:

  1. 冒泡排序和选择排序;
  2. 插入排序
  3. 希尔排序;
  4. 归并排序及其优化;
  5. 快速排序及其优化;
  6. 堆排序;

下面开始正文:

冒泡排序

冒泡排序可能是我们学习某种编程语言(大多数都是 C)之后,接触到的第一个排序算法。我在网上找了很多的博客之类的介绍冒泡排序的,但总是总是有点纰漏,比如 分析的和写的例子不一致(😂😂😂),很尴尬。下面我们详细来分析一下冒泡排序的过程:

待排序数组为:

1
{6, 2, 4, 1, 5, 9 }

冒泡排序的原理是:临近的``数字两两进行比较,按照从小到大或者从大到小的顺序进行交换。

这里注意:是临近的两个数字两两进行比较,如果非,那就不是冒泡排序。

起始数组如下:

起始数组

  1. 第一趟排序

第一次比较: 6 > 2 6 大于 2 进行交换,交换后:

00

第二次比较: 6 > 4 6 大于 4 进行交换,交换后:

第三次比较: 6 > 1 6 大于 1 进行交换,交换后:

第四次比较: 6 > 5 6 大于 5 进行交换,交换后:

第五次比较: 6 < 9 6 小于 9 不进行交换:

后面的第二、三、四、五、六的过程和上面的一样,最终如下面:

注意:第二次比较的时候,最后一位就不参与了,因为最大的已经冒了出来,第三次比较,倒数第二位不参与了,以此类推。

至此,程序就很容易写出来了:

1
2
3
4
5
6
7
8
9
void bubbleSort(int arr[],int n) {
for (int i = 0; i < n ; i ++) {
for (int j = 1; j < n-i; j ++) {
if (arr[j] < arr[j-1]) {
swap(&arr[j], &arr[j-1]);
}
}
}
}

冒泡排序的优化

我们看一下 每次排序的结果:

1
2
3
4
5
6
第一次:2 4 1 5 6 9 
第二次:2 1 4 5 6 9
第三次:1 2 4 5 6 9
第四次:1 2 4 5 6 9
第五次:1 2 4 5 6 9
第六次:1 2 4 5 6 9

从中我们发现第三次的时候,已经排好序了,第四、五、六次没必要执行了,这就是我们要对冒泡排序进行优化的地方。

我们可以设置一个flag ,当没有发生交换的时候,记录一下,这时候数组已经有序了。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void bubbleSort2(int arr[],int n) {
int flag = 1;
for (int i = 0; i < n && flag ; i ++) {
flag = 0;
for (int j = 1; j < n - i; j ++) {

if (arr[j] < arr[j-1]) {
flag = 1;
swap(&arr[j], &arr[j-1]);
}
}
}
}

当然,还有其他的优化方案,大家不妨搜下。

选择排序

理解的前面的冒泡排序,选择排序是很容易理解的。选择排序顾名思义,每次循环要找出一个最小的值,然后再交换。

测试用例还是之前的:

起始数组

第一次大循环找到最小的值 1 然后和 第0位的交换,交换后:

第二次大循环找到最小的值 2 然后和 第1位的交换,交换后:

当然第三次大循环和第二次一样

第四次大循环找到最小的值 5 然后和 第3位的交换,交换后:


后面的就已经排序好了。

从上面可以看出,选择排序是一个不断寻找最小值的排序方法。

所以代码如下:

1
2
3
4
5
6
7
8
9
10
11
void selectSort(int arr[],int n) {
for (int i = 0; i < n; i++) {
int minIndex = i;
for (int j = i+1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
swap(&arr[i], &arr[minIndex]);
}
}

是不是很简单 😁😁😁!

最后附上Demo

iOS多线程

前言

在这篇文章中我整理了iOS多线程开发需要的一些知识点、方法和事项。本文主要讲述三部分 NSThread、Operation 和 GCD。

本文不算是原创,是一些我觉得写的比较有质量文章的综合,文中摘抄了这些文章中的部分内容,在文中和文章末尾我已标注出处,之所以要摘抄是因为在需要之时,打开URL 是404 😂😂😂。我综合了这些文中给自己学习之用,也给大家方便查看之用,如有错误之处,见谅,不过欢迎指出。

基本概念

我们先了解一线iOS多线程的一些基本概念(这些基本概念参考了这里):

  • 进程(process):指的是一个正在运行中的可执行文件。每一个进程都拥有独立的虚拟内存空间和系统资源,包括端口权限等,且至少包含一个主线程和任意数量的辅助线程(说白了就是:用于指代一个可执行程序,他可以包含多个线程)。另外,当一个进程的主线程退出时,这个进程就结束了;
  • 线程(thread):指的是一个独立的代码执行路径,也就是说线程是代码执行路径的最小分支。在 iOS 中,线程的底层实现是基于 POSIX threads API 的,也就是我们常说的 pthreads ;
  • 任务(task),指的是我们需要执行的工作,是一个抽象的概念,用通俗的话说,就是一段代码。


串行 和 并发

从本质上来说,串行和并发的主要区别在于允许同时执行的任务数量。

  • 串行:指的是一次只能执行一个任务,必须等一个任务执行完成后才能执行下一个任务;
  • 并发:指的是允许多个任务在一段时间内同时执行。


同步 和 异步

同步和异步操作的主要区别在于是否等待操作执行完成,亦即是否阻塞当前线程。

  • 同步操作:等待操作执行完成后再继续执行接下来的代码;
  • 异步操作:它会在调用后立即返回,不会等待操作的执行结果。


队列 和 线程

在 iOS 中,有两种不同类型的队列,分别是串行队列和并发队列。正如我们上面所说的,串行队列一次只能执行一个任务,而并发队列则可以允许多个任务同时执行。iOS 系统就是使用这些队列来进行任务调度的,它会根据调度任务的需要和系统当前的负载情况动态地创建和销毁线程,而不需要我们手动地管理


iOS中的几种多线程类型

iOS 中其实目前有 4 种多线程,他们分别是:

  • Pthreads:非常底层的东东;
  • NSThread:封装性最差,最偏向于底层,主要基于thread使用;
  • GCD:基于C的API,直接使用比较方便,主要基于task使用;
  • NSOperation & NSOperationQueue:基于GCD封装的NSObject对象,对于复杂的多线程项目使用比较方便,主要基于队列使用。

下面开始介绍这几种类型

Pthreads

POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程。

这个需要自己管理线程的生命周期,创建和销毁,写起来也是相当麻烦,若非是写一些非常底层的几乎是用不到的,这里暂时略过,如需要可自行去理解。

NSThread

NSThread是苹果封装的,并且是面向对象的,这对我们来说就简便了许多,但是它的生命周期还是需要我们手动管理的;NSThread除Pthreads之外唯一一个基于线程封装的,每一个NSThread对象代表着一个线程。(这部分参考了这里)

线程的创建

NSThread提供了2种创建线程的方法:

1
2
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument
  • detach方法直接创建并启动一个线程去Selector,由于没有返回值,如果需要获取新创建的Thread,需要在执行的Selector中调用-[NSThread currentThread]获取;
  • init方法初始化线程并返回,线程的入口函数由Selector传入。线程创建出来之后需要手动调用-start方法启动;

    线程操作

    NSThread给线程提供的主要操作方法有启动,睡眠,取消,退出

    启动

    使用init方法将线程创建出来之后,线程并不会立即运行,需要手动调用-start方法才会启动线程:
1
- (void)start NS_AVAILABLE(10_5, 2_0);

在启动之前可以设置 线程的名称(当然启动后也可以在Selecter 中设置也可以):

1
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);

不过优先级 需要在 start 之前 设置,否则会无效:

1
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);


睡眠

NSThread提供了2个让线程睡眠的方法:

  • 根据NSDate传入睡眠时间;
  • 直接传入NSTimeInterval
1
2
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

这里讲一下 sleepUntilDate: 和 runloop的runUntilDate: 上的一些区别:

  • sleepUntilDate:相当于执行一个sleep的任务。在执行过程中,即使有其他任务传入runloop,runloop也不会立即响应,必须sleep任务完成之后,才会响应其他任务;
  • runUntilDate:虽然会阻塞线程,阻塞过程中并不妨碍新任务的执行。当有新任务的时候,会先执行接收到的新任务,新任务执行完之后,如果时间到了,再继续执行runUntilDate:之后的代码;


取消

NSThread提供了一个取消的方法和一个属性:

1
2
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);
- (void)cancel NS_AVAILABLE(10_5, 2_0);

这里 cancel 方法要注意:调用-cancel方法并不会立刻取消线程,它仅仅是将cancelled属性设置为YES。cancelled也仅仅是一个用于记录状态的属性。线程取消的功能需要我们在main函数中自己实现;

要实现取消的功能,我们需要自己在线程的main函数中定期检查isCancelled状态来判断线程是否需要退出,当isCancelled为YES的时候,需要手动退出。

退出

-exit函数可以让线程立即退出。

停止方法会立即终止除主线程以外所有线程(无论是否在执行任务)并退出,需要在掌控所有线程状态的情况下调用此方法,否则可能会导致内存问题。

1
+ (void)exit;


主线程和当前线程

NSThread提供了非常方便的获取和判断主线程的API:

1
2
3
@property (readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0);
+ (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
+ (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);

获取当前线程:

1
+ (NSThread *)currentThread;

是否为多线程:

1
+ (BOOL)isMultiThreaded;

关于isMultiThreaded 苹果在这里作了解释(相信你能看懂 😜😜😜):

1
2
3
4
5
Return Value
YES if the application is multithreaded, NO otherwise.

Discussion
An application is considered multithreaded if a thread was ever detached from the main thread using either detachNewThreadSelector:toTarget:withObject: or start. If you detached a thread in your application using a non-Cocoa API, such as the POSIX or Multiprocessing Services APIs, this method could still return NO. The detached thread does not have to be currently running for the application to be considered multithreaded—this method only indicates whether a single thread has been spawned.


线程优先级

NSThread有4个优先级的API:

这两个用于设置和获取当前线程的优先级:

1
2
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;

后两个通过对象设置和获取优先级:

1
2
@property double threadPriority NS_AVAILABLE(10_6, 4_0); // To be deprecated; use qualityOfService below
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0); // read-only after the thread is started

在iOS 8 之前:

1
[NSThread setThreadPriority:1.0];

这个方法的优先级的数值设置让人困惑,因为你不知道你应该设置多大的值是比较合适的,因此在iOS8之后,threadPriority添加了一句注释:To be deprecated; use qualityOfService below

意为iOS 8以后推荐使用qualityOfService属性:

1
2
3
4
5
6
7
typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21, 最高优先级,用于用户交互事件
NSQualityOfServiceUserInitiated = 0x19, 次高优先级,用于用户需要马上执行的事件
NSQualityOfServiceUtility = 0x11, 普通优先级,用于普通任务
NSQualityOfServiceBackground = 0x09, 最低优先级,用于不重要的任务
NSQualityOfServiceDefault = -1 默认优先级,主线程和没有设置优先级的线程都默认为这个优先级
} NS_ENUM_AVAILABLE(10_10, 8_0);

比如给线程设置次高优先级:

1
[newThread setQualityOfService:NSQualityOfServiceUserInitiated];


线程通讯

创建线程之后,经常需要从主线程把耗时的任务丢给辅助线程,当任务完成之后辅助线程再把结果传回主线程传,这些线程通讯一般用的都是perform方法:

1
2
3
4
5
6
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// equivalent to the first method with kCFRunLoopCommonModes

①:将selector丢给主线程执行,可以指定runloop mode

②:将selector丢给主线程执行,runloop mode默认为common mode

③:将selector丢个指定线程执行,可以指定runloop mode

④:将selector丢个指定线程执行,runloop mode默认为default mode

一般用③④方法将任务丢给辅助线程,任务执行完成之后再使用①②方法将结果传回主线程

注意:perform方法只对拥有runloop的线程有效,如果创建的线程没有添加runloop,perform的selector将无法执行。


线程通知

NSThread有三个线程相关的通知:

1
2
3
FOUNDATION_EXPORT NSString * const NSWillBecomeMultiThreadedNotification;
FOUNDATION_EXPORT NSString * const NSDidBecomeSingleThreadedNotification;
FOUNDATION_EXPORT NSString * const NSThreadWillExitNotification;
  • NSWillBecomeMultiThreadedNotification:由当前线程派生出第一个其他线程时发送;
  • NSDidBecomeSingleThreadedNotification:暂时不知道;
  • NSThreadWillExitNotification:线程退出时发送;


NSThread的简单使用

NSThread 创建还是很简单的:

1
2
3
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadAction) object:nil];
thread.name = @"com.myCreate.www";
[thread start];

在线程启动之后会首先执行-threadAction,正常情况下threadAction方法执行结束之后,线程就会退出。为了线程可以长期复用接收消息,我们需要给线程加上runLoop

1
2
3
4
5
6
7
8
9
- (void)threadAction
{
NSLog(@"当前线程:%@", [NSThread currentThread]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (![[NSThread currentThread] isCancelled]) {
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}
}
  • 自定义的线程默认是没有runloop的,调用-currentRunLoop,方法内部会为线程创建runloop;
  • 如果没有数据源,runloop会在启动之后会立刻退出。所以需要给runloop添加一个数据源,这里添加的是NSPort数据源;
  • 定期检查isCancelled,当外部调用-cancel方法将isCancelled置为YES的时候,线程可以退出;


结束线程

当我们想要结束线程的时候,我们可以使用CFRunLoopStop()配合-cancel来结束线程:

1
2
[[NSThread currentThread] cancel];
CFRunLoopStop(CFRunLoopGetCurrent());


NSThread的一个小例子

这个例子很好的说明了NSThread使用时要注意些什么。

这个例子是:模拟售票这个例子来自这

情景:某演唱会门票发售,在广州和北京均开设窗口进行销售

下面是主要代码:新建两个线程

1
2
3
4
5
6
7
8
NSLog(@"***************售票开始****************");
NSThread *saleTicketsWindow_1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicketsWindow_Action) object:nil];
saleTicketsWindow_1.name = @"北京售票中心";
[saleTicketsWindow_1 start];

NSThread *saleTicketsWindow_2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicketsWindow_Action) object:nil];
saleTicketsWindow_2.name = @"广州售票中心";
[saleTicketsWindow_2 start];

开始执行售票

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)saleTicketsWindow_Action
{
while (1) {
if (_ticketCount > 0) {
_ticketCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"***************售票完成****************");
break;
}
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2016-09-10 17:54:22.636 NSThreadTest[2323:1382484] 剩余票数:9 窗口:广州售票中心
2016-09-10 17:54:22.636 NSThreadTest[2323:1382483] 剩余票数:10 窗口:北京售票中心
2016-09-10 17:54:22.841 NSThreadTest[2323:1382483] 剩余票数:7 窗口:北京售票中心
2016-09-10 17:54:22.841 NSThreadTest[2323:1382484] 剩余票数:8 窗口:广州售票中心
2016-09-10 17:54:23.043 NSThreadTest[2323:1382484] 剩余票数:5 窗口:广州售票中心
2016-09-10 17:54:23.043 NSThreadTest[2323:1382483] 剩余票数:6 窗口:北京售票中心
2016-09-10 17:54:23.244 NSThreadTest[2323:1382484] 剩余票数:4 窗口:广州售票中心
2016-09-10 17:54:23.244 NSThreadTest[2323:1382483] 剩余票数:3 窗口:北京售票中心
2016-09-10 17:54:23.449 NSThreadTest[2323:1382483] 剩余票数:1 窗口:北京售票中心
2016-09-10 17:54:23.449 NSThreadTest[2323:1382484] 剩余票数:2 窗口:广州售票中心
2016-09-10 17:54:23.655 NSThreadTest[2323:1382483] 剩余票数:0 窗口:北京售票中心
2016-09-10 17:54:23.655 NSThreadTest[2323:1382484] ***************售票完成****************
2016-09-10 17:54:23.656 NSThreadTest[2323:1382484] 线程退出了:广州售票中心
2016-09-10 17:54:23.861 NSThreadTest[2323:1382483] ***************售票完成****************
2016-09-10 17:54:23.861 NSThreadTest[2323:1382483] 线程退出了:北京售票中心

可以看到,票的销售过程中出现了剩余数量错乱的情况,这就是线程同步问题。

售票是一个典型的需要线程同步的场景,由于售票渠道有很多,而票的资源是有限的,当多个渠道在短时间内卖出大量的票的时候,如果没有同步机制来管理票的数量,将会导致票的总数和售出票数对应不上的错误。

iOS实现线程加锁有几种方式,现在使用NSLock或@synchronized两种方式都可行

使用NSLock:

1
2
3
4
5
6
7
8
9
10
11
12
13
while (1) {
[_lock lock];
if (_ticketCount > 0) {
_ticketCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"***************售票完成****************");
[_lock unlock];
break;
}
[_lock unlock];
}

使用@synchronized:

1
2
3
4
5
6
7
8
9
10
11
12
while (1) {
@synchronized(self) {
if (_ticketCount > 0) {
_ticketCount --;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", _ticketCount, [NSThread currentThread].name]);
[NSThread sleepForTimeInterval:0.2];
} else {
NSLog(@"***************售票完成****************");
break;
}
}
}

NSOperation

NSOperation 只是一个抽象类,所以不能封装任务,因此,如果我们想要使用它来执行具体任务的话,就必须创建自己的子类或使用它的 2 个子类。分别是:NSInvocationOperation 和 NSBlockOperation 。创建一个 Operation 后,需要调用 start 方法来启动任务,它会 默认在当前队列同步执行。当然你也可以在中途取消一个任务,只需要调用其 cancel 方法即可。

NSInvocationOperation

下面是NSInvocationOperation的简单使用,可以看到 NSInvocationOperation 开始任务之后是在主线程执行任务的。

1
2
3
4
5
6
7
8
9
//1.创建对象
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationAction) object:nil];
//2.开始执行
[operation start];
//3.结果
/*
2016-05-06 05:53:43.219 NSOperationTest[5885:2491863] NSInvocationOperation --- <NSThread: 0x7fa269c06dc0>{number = 1, name = main}
是在主线程
*/

NSBlockOperation

NSBlockOperation 默认会在当前线程执行任务。但是 NSBlockOperation 还有一个方法:addExecutionBlock: ,通过这个方法可以给 Operation 添加多个执行 Block。这样 Operation 中的任务 会并发执行,它会 在主线程和其它的多个线程执行这些任务.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperation --- %@", [NSThread currentThread]);
}];
for (int i = 0; i < 5; i++) {
[blockOperation addExecutionBlock:^{
NSLog(@"NSBlockOperation 第%d次:%@", i, [NSThread currentThread]);
}];
}
[blockOperation start];

2016-05-06 05:57:46.227 NSBlockOperationTest[5921:2510961] NSBlockOperation --- <NSThread: 0x7fc4c0401af0>{number = 1, name = main}

2016-05-06 06:01:25.939 NSBlockOperationTest[5939:2525676] NSBlockOperation 第2次:<NSThread: 0x7fb6cbe04590>{number = 1, name = main}
2016-05-06 06:01:25.940 NSBlockOperationTest[5939:2525676] NSBlockOperation 第3次:<NSThread: 0x7fb6cbe04590>{number = 1, name = main}
2016-05-06 06:01:25.940 NSBlockOperationTest[5939:2525676] NSBlockOperation 第4次:<NSThread: 0x7fb6cbe04590>{number = 1, name = main}
2016-05-06 06:01:25.940 NSBlockOperationTest[5939:2525717] NSBlockOperation 第0次:<NSThread: 0x7fb6cbf03040>{number = 2, name = (null)}
2016-05-06 06:01:25.940 NSBlockOperationTest[5939:2525710] NSBlockOperation 第1次:<NSThread: 0x7fb6cbc3f6e0>{number = 3, name = (null)}

注意:addExecutionBlock 方法必须在 start() 方法之前执行,否则就会报错:

1
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSBlockOperation addExecutionBlock:]: blocks cannot be added after the operation has started executing or finished'

NSOperationQueue

在 NSOperationQueue 中,任务不会在当前线程执行。当任务添加到队列,会自动调用任务的 start() 方法,所有任务是并行执行的。maxConcurrentOperationCount 可以设置最大任务并行数量,当设置为 1 时,在某种意义上就是串行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperation --- %@", [NSThread currentThread]);
}];
for (int i = 0; i < 5; i++) {
[blockOperation addExecutionBlock:^{
NSLog(@"NSBlockOperation 第%d次:%@", i, [NSThread currentThread]);
}];
}
[queue addOperation:blockOperation];
或者是:
[queue addOperationWithBlock:^{
for (int i = 0; i < 5; i++) {
NSLog(@"addOperationWithBlock 第%d次:%@", i, [NSThread currentThread]);
}
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"addOperationWithBlock %@", [NSThread currentThread]);
}];
}];

eg:NSOperation 有一个非常实用的功能,那就是添加依赖。比如有 3 个任务:A: 从服务器上下载一张图片,B:给这张图片加个水印,C:把图片返回给服务器。这时就可以用到依赖了:

1
2
3
4
5
6
7
8
9
10
11
12
13
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载图片 --- %@", [NSThread currentThread]);
}];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"打水印 --- %@", [NSThread currentThread]);
}];
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"上传图片 --- %@", [NSThread currentThread]);
}];
[operation2 addDependency:operation1];
[operation3 addDependency:operation2];
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperations:@[operation1,operation2,operation3] waitUntilFinished:NO];

结果:

1
2
3
2016-05-06 06:26:12.732 NSOperationQueueTest[6061:2650250] 下载图片 --- <NSThread: 0x7fc589e1d740>{number = 3, name = (null)}
2016-05-06 06:26:12.733 NSOperationQueueTest[6061:2650257] 打水印 --- <NSThread: 0x7fc589f01320>{number = 4, name = (null)}
2016-05-06 06:26:12.734 NSOperationQueueTest[6061:2650257] 上传图片 --- <NSThread: 0x7fc589f01320>{number = 4, name = (null)}

注意:不能添加相互依赖,会死锁,比如 A依赖B,B依赖A。

可以使用 removeDependency 来解除依赖关系。

可以在不同的队列之间依赖,反正就是这个依赖是添加到任务身上的,和队列没关系。

另外还有以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
NSOperation
BOOL executing; //判断任务是否正在执行
BOOL finished; //判断任务是否完成
void (^completionBlock)(void); //用来设置完成后需要执行的操作
- (void)cancel; //取消任务
- (void)waitUntilFinished; //阻塞当前线程直到此任务执行完毕

NSOperationQueue

NSUInteger operationCount; //获取队列的任务数
- (void)cancelAllOperations; //取消队列中所有的任务
- (void)waitUntilAllOperationsAreFinished; //阻塞当前线程直到此队列中的所有任务执行完毕
[queue setSuspended:YES]; // 暂停queue
[queue setSuspended:NO]; // 继续queue

关于NSOperationQueue中 :

1
@property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue NS_AVAILABLE(10_10, 8_0);

在这里有介绍其用法

这里也有解释

这里是苹果的解释:

1
2
3
4
Discussion
The default value of this property is nil. You can set the value of this property to an existing dispatch queue to have enqueued operations interspersed with blocks submitted to that dispatch queue.

The value of this property should only be set if there are no operations in the queue; setting the value of this property when operationCount is not equal to 0 raises an invalidArgumentException. The value of this property must not be the value returned by dispatch_get_main_queue(). The quality-of-service level set for the underlying dispatch queue overrides any value set for the operation queue's qualityOfService property.

自定义Operation

后面会有专门有一节介绍


GCD

Grand Central Dispatch 是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的CPU内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理,我们只需要告诉干什么就行。

在 GCD 中,加入了两个非常重要的概念: 任务 和 队列。 任务即你要执行的,同步、异步和队列 前面已经讲解过。

放到串行队列的任务,GCD 会 FIFO(先进先出) 地取出来一个,执行一个,然后取下一个,这样一个一个的执行。<br/>

放到并行队列的任务,GCD 也会 FIFO的取出来,但不同的是,它取出来一个就会放到别的线程,然后再取出来一个又放到另一个的线程。这样由于取的动作很快,忽略不计,看起来,所有的任务都是一起执行的。不过需要注意,GCD 会根据系统资源控制并行的数量,所以如果任务很多,它并不会让所有任务同时执行。<br/>

创建队列

先讲概念:

GCD中的FIFO队列称为dispatch queue,用来保证先进来的任务先得到执行。

dispatch queue分三种

  • Serial:又叫private dispatch queues,同时只执行一个任务。Serial queue常用于同步访问特定的资源或数据。当你创建多个Serial queue时,虽然各自是同步,但serial queue之间是并发执行。(DISPATCH_QUEUE_SERIAL)
  • Concurrent:又叫global dispatch queue,可以并发的执行多个任务,但执行完成顺序是随机的。(DISPATCH_QUEUE_CONCURRENT)
  • Main dispatch queue:全局可用的serial queue,在应用程序主线程上执行任务。

公开的5个不同队列:
运行在主线程中的main queue,
3个不同优先级的后台队列(High Priority Queue,Default Priority Queue,Low Priority Queue),
以及一个优先级更低的后台队列Background Priority Queue(用于I/O)

5种队列,主队列(main queue),四种通用调度队列,自己定制的队列。四种通用调度队列为

  • QOS_CLASS_USER_INTERACTIVE:user interactive等级表示任务需要被立即执行提供好的体验,用来更新UI,响应事件等。这个等级最好保持小规模。
  • QOS_CLASS_USER_INITIATED: user initiated等级表示任务由UI发起异步执行。适用场景是需要及时结果同时又可以继续交互的时候。
  • QOS_CLASS_UTILITY: utility等级表示需要长时间运行的任务,伴有用户可见进度指示器。经常会用来做计算,I/O,网络,持续的数据填充等任务。这个任务节能。
  • QOS_CLASS_BACKGROUND: background等级表示用户不会察觉的任务,使用它来处理预加载,或者不需要用户交互和对时间不敏感的任务。
主队列

这是一个特殊的 串行队列,它用于刷新 UI,任何需要刷新 UI 的工作都要在主队列执行.

1
dispatch_queue_t queue = dispatch_get_main_queue();
自定义的队列

其中第一个参数是标识符,用于 DEBUG 的时候标识唯一的队列,可以为空.

第二个参数用来表示创建的队列是串行的还是并行的,传入 DISPATCH_QUEUE_SERIAL 或 NULL 表示创建串行队列。传入 DISPATCH_QUEUE_CONCURRENT 表示创建并行队列。

1
2
3
dispatch_queue_t
dispatch_queue_create(const char *_Nullable label,
dispatch_queue_attr_t _Nullable attr);
1
2
3
4
5
// 串行队列
dispatch_queue_t createQueue = dispatch_queue_create("com.testQueue", NULL);
dispatch_queue_t createQueue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_SERIAL);
// 并行队列
dispatch_queue_t createQueue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);

系统还提供了 全局并行队列

1
dispatch_queue_t createQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

全局队列有这几种优先级:

1
2
3
4
#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
创建任务

同步任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dispatch_sync(queue, ^{

});
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"async之前 - %@",[NSThread currentThread]);
dispatch_sync(queue, ^{
NSLog(@"async1 - %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"async2 - %@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"async3 - %@",[NSThread currentThread]);
});
NSLog(@"async之后 - %@",[NSThread currentThread]);

log:

1
2
3
4
5
2016-05-05 07:03:01.137 syncTest[5181:1954382] async之前 - <NSThread: 0x7fd7e9400ec0>{number = 1, name = main}
2016-05-05 07:03:01.138 syncTest[5181:1954382] async1 - <NSThread: 0x7fd7e9400ec0>{number = 1, name = main}
2016-05-05 07:03:01.138 syncTest[5181:1954382] async2 - <NSThread: 0x7fd7e9400ec0>{number = 1, name = main}
2016-05-05 07:03:01.138 syncTest[5181:1954382] async3 - <NSThread: 0x7fd7e9400ec0>{number = 1, name = main}
2016-05-05 07:03:01.138 syncTest[5181:1954382] async之后 - <NSThread: 0x7fd7e9400ec0>{number = 1, name = main}

注意:

1.同步队列无论设置是DISPATCH_QUEUE_SERIAL还是DISPATCH_QUEUE_CONCURRENT,任务都是在主线程执行,会阻塞主线程

2.dispatch_sync 中 queue 不能是 dispatch_get_main_queue() 即为主线程队列,否则会creash

1
2
3
4
5
NSLog(@"dispatch_sync  enter");
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"dispatch_sync action");
});
NSLog(@"dispatch_sync leave");

异步任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch_async(createQueue, ^{

});
//并行队列 DISPATCH_QUEUE_CONCURRENT
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"async之前 - %@",[NSThread currentThread]);
dispatch_async(queue, ^{
NSLog(@"async1 - %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"async2 - %@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"async3 - %@",[NSThread currentThread]);
});
NSLog(@"async之后 - %@",[NSThread currentThread]);

log:

1
2
3
4
5
2016-09-21 07:19:53.817 asyncTest[13930:8566716] async之前 - <NSThread: 0x600000066a40>{number = 1, name = main}
2016-09-21 07:19:53.818 asyncTest[13930:8566716] async之后 - <NSThread: 0x600000066a40>{number = 1, name = main}
2016-09-21 07:20:59.683 asyncTest[13930:8566864] async2 - <NSThread: 0x60800007d700>{number = 4, name = (null)}
2016-09-21 07:20:59.683 asyncTest[13930:8566862] async3 - <NSThread: 0x60000007a9c0>{number = 3, name = (null)}
2016-09-21 07:20:59.683 asyncTest[13930:8566867] async1 - <NSThread: 0x60000007ad00>{number = 5, name = (null)}

队列组

创建队列

group 可以分为 串行和并行的,区别在于DISPATCH_QUEUE_SERIAL和DISPATCH_QUEUE_CONCURRENT设置不同

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});

log: 串行,但是异步的,不会阻塞当前线程

1
2
3
4
5
6
7
8
9
10
2016-09-21 08:05:14.110 GCDTest[14034:8745909] group-01 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:14.111 GCDTest[14034:8745909] group-01 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:14.112 GCDTest[14034:8745909] group-01 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:14.112 GCDTest[14034:8745909] group-01 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:14.112 GCDTest[14034:8745909] group-01 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:19.589 GCDTest[14034:8745909] group-03 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:19.590 GCDTest[14034:8745909] group-03 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:19.590 GCDTest[14034:8745909] group-03 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:19.591 GCDTest[14034:8745909] group-03 - <NSThread: 0x60000027d100>{number = 4, name = (null)}
2016-09-21 08:05:19.591 GCDTest[14034:8745909] group-03 - <NSThread: 0x60000027d100>{number = 4, name = (null)}

并行:

1
2
3
4
5
6
7
8
9
10
2016-09-21 08:09:46.984 GCDTest[14058:8768392] group-03 - <NSThread: 0x608000273900>{number = 4, name = (null)}
2016-09-21 08:09:46.984 GCDTest[14058:8768371] group-01 - <NSThread: 0x608000273200>{number = 3, name = (null)}
2016-09-21 08:09:46.984 GCDTest[14058:8768392] group-03 - <NSThread: 0x608000273900>{number = 4, name = (null)}
2016-09-21 08:09:46.984 GCDTest[14058:8768371] group-01 - <NSThread: 0x608000273200>{number = 3, name = (null)}
2016-09-21 08:09:46.985 GCDTest[14058:8768392] group-03 - <NSThread: 0x608000273900>{number = 4, name = (null)}
2016-09-21 08:09:46.985 GCDTest[14058:8768371] group-01 - <NSThread: 0x608000273200>{number = 3, name = (null)}
2016-09-21 08:09:46.985 GCDTest[14058:8768392] group-03 - <NSThread: 0x608000273900>{number = 4, name = (null)}
2016-09-21 08:09:46.985 GCDTest[14058:8768371] group-01 - <NSThread: 0x608000273200>{number = 3, name = (null)}
2016-09-21 08:09:46.985 GCDTest[14058:8768392] group-03 - <NSThread: 0x608000273900>{number = 4, name = (null)}
2016-09-21 08:09:46.986 GCDTest[14058:8768371] group-01 - <NSThread: 0x608000273200>{number = 3, name = (null)}
监听任务组事件的执行完毕

1.dispatch_group_notify

用来监听任务组事件的执行完毕, 异步执行闭包,不会阻塞

1
2
3
4
void
dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block)
;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-01-thread"];
for (int i = 0; i < 3; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
});

dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-03-thread"];
for (int i = 0; i < 3; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});

NSLog(@"notify leave");
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"notify 完成 - %@", [NSThread currentThread]);
});

NSLog(@"notify leave");

log:

1
2
3
4
5
6
7
8
9
2016-09-22 07:03:18.150 GCDTest[19000:9806166] notify leave
2016-09-22 07:03:18.150 GCDTest[19000:9813919] group-01 - <NSThread: 0x60000026b700>{number = 5, name = group-01-thread}
2016-09-22 07:03:18.150 GCDTest[19000:9814304] group-03 - <NSThread: 0x608000277800>{number = 6, name = group-03-thread}
2016-09-22 07:03:18.151 GCDTest[19000:9806166] notify leave
2016-09-22 07:03:18.151 GCDTest[19000:9813919] group-01 - <NSThread: 0x60000026b700>{number = 5, name = group-01-thread}
2016-09-22 07:03:18.151 GCDTest[19000:9814304] group-03 - <NSThread: 0x608000277800>{number = 6, name = group-03-thread}
2016-09-22 07:03:18.152 GCDTest[19000:9813919] group-01 - <NSThread: 0x60000026b700>{number = 5, name = group-01-thread}
2016-09-22 07:03:18.152 GCDTest[19000:9814304] group-03 - <NSThread: 0x608000277800>{number = 6, name = group-03-thread}
2016-09-22 07:03:18.157 GCDTest[19000:9806166] notify 完成 - <NSThread: 0x60800007b6c0>{number = 1, name = main}

2.dispatch_group_wait

会阻塞当前进程,等所有任务都完成或等待超时。设置等待时间,在等待时间结束后,如果还没有执行完任务组,则返回。返回0代表执行成功,非0则执行失败

1
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

超时时间 可以根据需要设置,系统提供了两个宏:

1
2
#define DISPATCH_TIME_NOW (0ull)
#define DISPATCH_TIME_FOREVER (~0ull)

在此之前介绍一下时间:

iOS系统 GCD 中提供了一下几种时间

1
2
3
4
#define NSEC_PER_SEC  1000000000ull
#define NSEC_PER_MSEC 1000000ull
#define USEC_PER_SEC 1000000ull
#define NSEC_PER_USEC 1000ull

关键词解释:

  • NSEC:纳秒
  • USEC:微秒
  • MSEC:毫秒
  • SEC:秒
  • PER:每

故:

描述
NSEC_PER_SEC 每秒有多少纳秒
NSEC_PER_MSEC 每毫秒有多少纳秒
USEC_PER_SEC 每秒有多少微秒
NSEC_PER_USEC 每微秒有多少纳秒

所以,延时1秒可以写成如下几种:

1
2
3
4
dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC);
dispatch_time(DISPATCH_TIME_NOW, 1000 * USEC_PER_SEC)
;

dispatch_time(DISPATCH_TIME_NOW, USEC_PER_SEC * NSEC_PER_USEC);
dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC * NSEC_PER_USEC)
;

绝对时间,dispatch_group_wait 想等到2016年09月22日23:20:53 的时候超时,那么要使用 dispatch_walltime函数将NSdate 转化为dispatch_time,下面是方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (dispatch_time_t)switchDateToDispatch_time_tWithDate:(NSDate *)date
{
NSTimeInterval interval;
double second,subsecond;
struct timespec time;
dispatch_time_t milestone;
interval=[date timeIntervalSince1970];

subsecond = modf(interval, &second);
time.tv_sec = second;
time.tv_nsec= subsecond*NSEC_PER_SEC;

milestone = dispatch_walltime(&time, 0);
return milestone;
}

好了时间说完了,该说示例了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-01-thread"];
for (int i = 0; i < 3; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
});
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-03-thread"];
for (int i = 0; i < 3; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});
NSLog(@"dispatch_group_wait 之前");
long value = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"dispatch_group_wait 之后 value = %ld",value);

log:

① DISPATCH_TIME_FOREVER 可以看到会阻塞当前线程直到任务全部完成

1
2
3
4
5
6
7
8
2016-09-22 07:15:14.211 GCDTest[19022:9870397] dispatch_group_wait 之前
2016-09-22 07:15:14.211 GCDTest[19022:9874488] group-01 - <NSThread: 0x608000268f40>{number = 5, name = group-01-thread}
2016-09-22 07:15:14.211 GCDTest[19022:9874995] group-03 - <NSThread: 0x608000268500>{number = 6, name = group-03-thread}
2016-09-22 07:15:14.211 GCDTest[19022:9874488] group-01 - <NSThread: 0x608000268f40>{number = 5, name = group-01-thread}
2016-09-22 07:15:14.211 GCDTest[19022:9874995] group-03 - <NSThread: 0x608000268500>{number = 6, name = group-03-thread}
2016-09-22 07:15:14.211 GCDTest[19022:9874488] group-01 - <NSThread: 0x608000268f40>{number = 5, name = group-01-thread}
2016-09-22 07:15:14.212 GCDTest[19022:9874995] group-03 - <NSThread: 0x608000268500>{number = 6, name = group-03-thread}
2016-09-22 07:15:14.212 GCDTest[19022:9870397] dispatch_group_wait 之后 value = 0

② DISPATCH_TIME_NOW dispatch_group_wait 并未阻塞当前线程 但是 返回非零值,表明任务执行失败或者超时,这明显是超时

1
2
3
4
5
6
7
8
2016-09-22 07:18:21.506 GCDTest[19041:9887622] dispatch_group_wait 之前
2016-09-22 07:18:21.506 GCDTest[19041:9891849] group-01 - <NSThread: 0x608000274700>{number = 3, name = group-01-thread}
2016-09-22 07:18:21.506 GCDTest[19041:9891894] group-03 - <NSThread: 0x608000274680>{number = 4, name = group-03-thread}
2016-09-22 07:18:21.506 GCDTest[19041:9887622] dispatch_group_wait 之后 value = 49
2016-09-22 07:18:21.506 GCDTest[19041:9891849] group-01 - <NSThread: 0x608000274700>{number = 3, name = group-01-thread}
2016-09-22 07:18:21.507 GCDTest[19041:9891894] group-03 - <NSThread: 0x608000274680>{number = 4, name = group-03-thread}
2016-09-22 07:18:21.507 GCDTest[19041:9891849] group-01 - <NSThread: 0x608000274700>{number = 3, name = group-01-thread}
2016-09-22 07:18:21.508 GCDTest[19041:9891894] group-03 - <NSThread: 0x608000274680>{number = 4, name = group-03-thread}

③ 自定义时间 会阻塞当前的线程

1
2
3
4
5
6
7
8
9
//这里让线程 睡眠2s
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-03-thread"];
[NSThread sleepForTimeInterval:2];
for (int i = 0; i < 3; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});
long value = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5*NSEC_PER_SEC)));
1
2
3
4
5
6
7
8
2016-09-22 07:37:32.478 GCDTest[19217:10005760] dispatch_group_wait 之前
2016-09-22 07:37:32.478 GCDTest[19217:10006060] group-01 - <NSThread: 0x608000274140>{number = 3, name = group-01-thread}
2016-09-22 07:37:32.479 GCDTest[19217:10006060] group-01 - <NSThread: 0x608000274140>{number = 3, name = group-01-thread}
2016-09-22 07:37:32.479 GCDTest[19217:10006060] group-01 - <NSThread: 0x608000274140>{number = 3, name = group-01-thread}
2016-09-22 07:37:34.482 GCDTest[19217:10006057] group-03 - <NSThread: 0x608000273f80>{number = 4, name = group-03-thread}
2016-09-22 07:37:34.483 GCDTest[19217:10006057] group-03 - <NSThread: 0x608000273f80>{number = 4, name = group-03-thread}
2016-09-22 07:37:34.483 GCDTest[19217:10006057] group-03 - <NSThread: 0x608000273f80>{number = 4, name = group-03-thread}
2016-09-22 07:37:34.484 GCDTest[19217:10005760] dispatch_group_wait 之后 value = 0

3.dispatch_group_enter dispatch_group_leave
这里 dispatch_group_enter 和 dispatch_group_leave 需要成对出现,否则dispatch_group_notify不会回调用(这里可能会崩溃,不过在我测试中没有发现)

①这是 dispatch_queue_t 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-01-thread"];
for (int i = 0; i < 3; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
[[NSThread currentThread] setName:@"group-03-thread"];
[NSThread sleepForTimeInterval:2];
for (int i = 0; i < 3; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
dispatch_group_leave(group);
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"notify 完成 - %@", [NSThread currentThread]);
});

②这是NSOperationQueue的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dispatch_group_t group = dispatch_group_create();
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
dispatch_group_enter(group);
NSBlockOperation *blockOperation_1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperation 1 --- %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:2];
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
NSBlockOperation *blockOperation_2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperation 2 --- %@", [NSThread currentThread]);
dispatch_group_leave(group);
}];
[blockOperation_1 setName:@"blockOperation_1"];
[blockOperation_1 setName:@"blockOperation_2"];
[queue addOperation:blockOperation_1];
[queue addOperation:blockOperation_2];
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"notify 完成 - %@", [NSThread currentThread]);
});
dispatch_once

dispatch_once 大家都不会陌生,但我认为还是有必要强调一次,dispatch_once_t 必须为静态变量

1
2
3
4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// to do something
});
dispatch_after

dispatch_after 能让我们添加进队列的任务延时(这里说延时不严谨,后面会说到)执行

1
2
3
4
5
6
NSLog(@"begin");
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^{
NSLog(@"end");
});

虽然可以提供NStimer 的功能,但它不是,看下面官方文档:

1
Enqueue a block for execution at the specified time.

Enqueue,就是入队,指的就是将一个Block在特定的延时以后,加入到指定的队列中,不是在特定的时间后立即运行!。这里有个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"begin");
dispatch_async(queue, ^{
[[NSThread currentThread] setName:@"group-01-thread"];
[NSThread sleepForTimeInterval:10];
NSLog(@"dispatch_group_async");
});
//1.使用 dispatch_after 延后工作
double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, queue, ^{
NSLog(@"dispatch_time");
});

log:

1
2
3
2016-09-23 22:56:05.258 GCDTest[20684:11209748] begin
2016-09-23 22:56:15.263 GCDTest[20684:11209796] dispatch_group_async
2016-09-23 22:56:15.263 GCDTest[20684:11209796] dispatch_time

从结果也验证了,dispatch_after只是延时提交block,并不是延时后立即执行。所以想用dispatch_after精确控制运行状态的朋友可要注意了~

dispatch_apply

dispatch_apply的作用是在一个队列(串行或并行)上“运行”多次block,不过会阻塞当前线程,按执行最长任务时间阻塞(异步,同步是所有任务时间总和),用时间要小心:

1
2
3
4
5
6
7
8
9
10
11
12
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"apply 之前");
dispatch_apply(3, queue, ^(size_t y) {
switch (y) {
case 0:[NSThread sleepForTimeInterval:1];break;
case 1:[NSThread sleepForTimeInterval:2];break;
case 2:[NSThread sleepForTimeInterval:3];break;
default:break;
}
NSLog(@"%ld",y);
});
NSLog(@"apply 之后");

log:

1
2
3
4
5
2016-09-23 23:30:24.984 GCDTest[20779:11373252] apply 之前
2016-09-23 23:30:25.985 GCDTest[20779:11373252] 0
2016-09-23 23:30:26.987 GCDTest[20779:11373585] 1
2016-09-23 23:30:27.988 GCDTest[20779:11373584] 2
2016-09-23 23:30:27.988 GCDTest[20779:11373252] apply 之后

将其放在异步线程去执行,这样就不阻塞主线程了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NSLog(@"dispatch_async 之前");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//使用dispatch_apply可以运行的更快
NSLog(@"apply 之前");
dispatch_apply(3, queue, ^(size_t y) {
switch (y) {
case 0:[NSThread sleepForTimeInterval:1];break;
case 1:[NSThread sleepForTimeInterval:2];break;
case 2:[NSThread sleepForTimeInterval:3];break;
default:break;
}
NSLog(@"%ld",y);
});
NSLog(@"apply 之后");
});
NSLog(@"dispatch_async 之后");

log:

1
2
3
4
5
6
7
2016-09-23 23:38:21.221 GCDTest[20817:11403936] dispatch_async 之前
2016-09-23 23:38:21.222 GCDTest[20817:11403936] dispatch_async 之后
2016-09-23 23:38:21.222 GCDTest[20817:11404070] apply 之前
2016-09-23 23:38:22.223 GCDTest[20817:11404070] 0
2016-09-23 23:38:23.226 GCDTest[20817:11404072] 1
2016-09-23 23:38:24.225 GCDTest[20817:11404069] 2
2016-09-23 23:38:24.226 GCDTest[20817:11404070] apply 之后
dispatch_barrier_async

dispatch_barrier_async的作用就是向某个队列插入一个block,当目前正在执行的block运行完成后,阻塞这个block后面添加的block,只运行这个block直到完成,然后再继续后续的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
dispatch_group_t group = dispatch_group_create();
// dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
NSLog(@"group-01 - %@", [NSThread currentThread]);
});


dispatch_barrier_async(queue, ^{
NSLog(@"barrier - %@", [NSThread currentThread]);
});



dispatch_group_async(group, queue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"group-02 - %@", [NSThread currentThread]);
}
});


dispatch_group_async(group, queue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});

log:

1
2
3
4
5
6
7
8
2016-09-23 23:54:08.478 GCDTest[20928:11478999] group-01 - <NSThread: 0x608000265700>{number = 3, name = (null)}
2016-09-23 23:54:08.478 GCDTest[20928:11478999] barrier - <NSThread: 0x608000265700>{number = 3, name = (null)}
2016-09-23 23:54:08.480 GCDTest[20928:11478999] group-02 - <NSThread: 0x608000265700>{number = 3, name = (null)}
2016-09-23 23:54:08.480 GCDTest[20928:11478997] group-03 - <NSThread: 0x60000026c680>{number = 4, name = (null)}
2016-09-23 23:54:08.481 GCDTest[20928:11478999] group-02 - <NSThread: 0x608000265700>{number = 3, name = (null)}
2016-09-23 23:54:08.481 GCDTest[20928:11478997] group-03 - <NSThread: 0x60000026c680>{number = 4, name = (null)}
2016-09-23 23:54:08.482 GCDTest[20928:11478999] group-02 - <NSThread: 0x608000265700>{number = 3, name = (null)}
2016-09-23 23:54:08.482 GCDTest[20928:11478997] group-03 - <NSThread: 0x60000026c680>{number = 4, name = (null)}
  • dispatchbarrier\(a)sync 只在自己创建的并发队列上有效,在全局(Global)并发队列、串行队列上,效果跟dispatch_(a)sync效果一样。
  • 既然在串行队列上跟dispatch_(a)sync效果一样,那就要小心别死锁!

在dispatch_get_global_queue:

1
2
3
4
5
6
7
8
2016-09-23 23:56:14.201 GCDTest[20946:11496633] group-01 - <NSThread: 0x608000263780>{number = 3, name = (null)}
2016-09-23 23:56:14.201 GCDTest[20946:11497029] group-02 - <NSThread: 0x600000277200>{number = 5, name = (null)}
2016-09-23 23:56:14.201 GCDTest[20946:11497030] group-03 - <NSThread: 0x608000265840>{number = 6, name = (null)}
2016-09-23 23:56:14.201 GCDTest[20946:11497028] barrier - <NSThread: 0x608000265680>{number = 4, name = (null)}
2016-09-23 23:56:14.201 GCDTest[20946:11497029] group-02 - <NSThread: 0x600000277200>{number = 5, name = (null)}
2016-09-23 23:56:14.201 GCDTest[20946:11497030] group-03 - <NSThread: 0x608000265840>{number = 6, name = (null)}
2016-09-23 23:56:14.202 GCDTest[20946:11497029] group-02 - <NSThread: 0x600000277200>{number = 5, name = (null)}
2016-09-23 23:56:14.202 GCDTest[20946:11497030] group-03 - <NSThread: 0x608000265840>{number = 6, name = (null)}
暂停与恢复
  • dispatch_suspend(queue); 暂停队列
  • dispatch_resume(queue); 恢复队列

dispatch_suspend函数对已经执行的处理没有影响。挂起后,追加到 Dispatch Queue 中但尚未执行的处理在此之后停止执行。而dispatch_resume则使得这些处理能够继续执行。

这里有个例子(参考来自这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
dispatch_queue_t queue1 = dispatch_queue_create("com.queue1", 0);
dispatch_queue_t queue2 = dispatch_queue_create("com.queue2", 0);
dispatch_group_t group = dispatch_group_create();
dispatch_async(queue1, ^{
NSLog(@"任务 1 : queue 1...");
sleep(1);
NSLog(@"✅完成任务 1");
});


dispatch_async(queue2, ^{
NSLog(@"任务 1 : queue 2...");
sleep(1);
NSLog(@"✅完成任务 2");
});



dispatch_group_async(group, queue1, ^{
NSLog(@"🚫正在暂停 1");
dispatch_suspend(queue1);
});

dispatch_group_async(group, queue2, ^{
NSLog(@"🚫正在暂停 2");
dispatch_suspend(queue2);
});

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"=======等待两个queue完成, 再往下进行...");
dispatch_async(queue1, ^{
NSLog(@"任务 2 : queue 1");
});

dispatch_async(queue2, ^{
NSLog(@"任务 2 : queue 2");
});

NSLog(@"🔴为什么这个NSLog会在上面两个NSLog之前打印❓❓答:dispatch_suspend的作用‼️");

dispatch_resume(queue1);
dispatch_resume(queue2);

log:

1
2
3
4
5
6
7
8
9
10
2016-09-24 22:12:58.646 GCDTest[22178:12407680] 任务 1queue 1...
2016-09-24 22:12:58.646 GCDTest[22178:12408707] 任务 1queue 2...
2016-09-24 22:12:59.650 GCDTest[22178:12407680] ✅完成任务 1
2016-09-24 22:12:59.650 GCDTest[22178:12408707] ✅完成任务 2
2016-09-24 22:12:59.650 GCDTest[22178:12407680] 🚫正在暂停 1
2016-09-24 22:12:59.650 GCDTest[22178:12408707] 🚫正在暂停 2
2016-09-24 22:12:59.650 GCDTest[22178:12407286] =======等待两个queue完成, 再往下进行...
2016-09-24 22:12:59.651 GCDTest[22178:12407286] 🔴为什么这个NSLog会在上面两个NSLog之前打印❓❓答:dispatch_suspend的作用‼️
2016-09-24 22:12:59.651 GCDTest[22178:12408707] 任务 2queue 1
2016-09-24 22:12:59.651 GCDTest[22178:12407680] 任务 2queue 2

Dispatch Source (信号源)

GCD中除了主要的 Dispatch Queue 外,还有不太引人注目的 Dispatch Source .它是BSD系内核惯有功能kqueue的包装。kqueue 是在 XNU 内核中发生各种事件时,在应用程序编程方执行处理的技术。其 CPU 负荷非常小,尽量不占用资源。kqueue 可以说是应用程序处理 XNU 内核中发生的各种事件的方法中最优秀的一种。

Dispatch Source是GCD中的一个基本类型,从字面意思可称为调度源,它的作用是当有一些特定的较底层的系统事件发生时,调度源会捕捉到这些事件,然后可以做其他的逻辑处理,调度源有多种类型,分别监听对应类型的系统事件。

Dispatch Source 可处理的所有事件。如下表所示:

名称 内容
DISPATCH_SOURCE_TYPE_DATA_ADD 变量增加
DISPATCH_SOURCE_TYPE_DATA_OR 变量OR
DISPATCH_SOURCE_TYPE_MACH_SEND MACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV MACH端口接收
DISPATCH_SOURCE_TYPE_PROC 监测到与进程相关的事件
DISPATCH_SOURCE_TYPE_READ 可读取文件映像
DISPATCH_SOURCE_TYPE_SIGNAL 接收信号
DISPATCH_SOURCE_TYPE_TIMER 定时器
DISPATCH_SOURCE_TYPE_VNODE 文件系统有变更
DISPATCH_SOURCE_TYPE_WRITE 可写入文件映像

其中DISPATCH_SOURCE_TYPE_DATA_ADD和DISPATCH_SOURCE_TYPE_DATA_OR是常用的两个,其它用于Mac开发的比较多。

在任一线程上调用它的的一个函数 dispatch_source_merge_data 后,会执行 Dispatch Source 事先定义好的句柄(可以把句柄简单理解为一个 block )。

这个过程叫 Custom event ,用户事件。是 dispatch source 支持处理的一种事件。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 dispatch_source_t queueSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
__block NSUInteger totalComplete = 0;
dispatch_source_set_event_handler(queueSource, ^{
//当处理事件被最终执行时,计算后的数据可以通过dispatch_source_get_data来获取。
//这个数据的值在每次响应事件执行后会被重置,所以totalComplete的值是最终累积的值。
NSUInteger value = dispatch_source_get_data(queueSource);
totalComplete += value;
NSLog(@"进度:%@", @((CGFloat)totalComplete/100));
});
/*
分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复。
因为忘记恢复分派源的状态而产生bug是常见的事儿。
恢复的方法是调用 dispatch_resume :
*/

dispatch_resume(queueSource);

/*
在每次循环中执行加1操作。也可以传递已处理记录的数目或已写入的字节数。
在任何线程中都可以调用 dispatch_source_merge_data 。
需要注意的是,不可以传递0值(事件不会被触发),同样也不可以传递负数(会无穷大)。
*/

dispatch_queue_t queue = dispatch_queue_create("com.sourcequeue", DISPATCH_QUEUE_CONCURRENT);
for (NSUInteger index = 0; index < 10; index++) {
dispatch_async(queue, ^{
dispatch_source_merge_data(queueSource, 1);
[NSThread sleepForTimeInterval:1];
});
}
NSLog(@"queue for 循环 完成");

log:

1
2
3
4
5
6
2016-09-25 09:35:20.128 GCDTest[22525:12717227] queue for 循环 完成
2016-09-25 09:35:20.129 GCDTest[22525:12718440] 进度:0.3
2016-09-25 09:35:20.129 GCDTest[22525:12718440] 进度:0.4
2016-09-25 09:35:20.129 GCDTest[22525:12718440] 进度:0.6
2016-09-25 09:35:20.130 GCDTest[22525:12718440] 进度:0.9
2016-09-25 09:35:20.130 GCDTest[22525:12718440] 进度:1

可以看到 0.1~1 并没有全部打印,而是跳过:

这是 DispatchSource 能通过合并事件的方式确保在高负载下正常工作 的能力

在同一时间,只有一个处理 block 的实例被分配,如果这个处理方法还没有执行完毕,另一个事件就发生了,事件会以指定方式(ADD或 OR)进行累积。DispatchSource能通过合并事件(block)的方式确保在高负载下正常工作。当处理事件被最终执行时,计算后的数据可以通过 dispatch_source_get_data 来获取。这个数据的值在每次响应时间执行后会被重置,所以上面的例子中进度条 totalComplete 的值是最终积累的值,而 block 不是每次都执行的。但能确保进度条能从0.0到1.0的正常执行。

Dispatch Semaphore 信号量

为了展示作用,举个反例:

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc] init];
dispatch_group_t group = dispatch_group_create();
for(int i = 0; i< 100000; ++i) {
dispatch_group_async(group, queue, ^{
[array addObject:[NSNumber numberWithInt:i]];
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"%@", @([array count]));

然后就发生了崩溃:

1
2
malloc: *** error for object 0x7f88ed03ca00: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug

这种资源抢夺的情况,一般的做法是使用串行队列,或者像下面一样的同步队列,得以解决:

1
2
3
4
5
6
7
8
9
10
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSMutableArray *array = [[NSMutableArray alloc] init];
dispatch_group_t group = dispatch_group_create();
for(int i = 0; i< 1000; ++i) {
dispatch_sync(queue, ^{
[array addObject:[NSNumber numberWithInt:i]];
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"%@", @([array count]));

dispatch_semaphore_t 的作用之一就是解决这种资源抢夺的情况,下面展示下展示使用 dispatch_semaphore_t 的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dispatch_queue_t queue = dispatch_queue_create("com.semaphore", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"🔴%@ index = %d",[NSThread currentThread],i);
[array addObject:@(i)];
dispatch_semaphore_signal(semaphore);
});
}
dispatch_barrier_async(queue, ^{
NSLog(@"🔴类名与方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @([array count]));
});

测试,设置:

1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

可以发现 程序会永远停留在:

1
2
3
等待Dispatch Semaphore
一直等待,直到Dispatch Semaphore的计数值达到大于等于1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

关于信号量这里有非常详细的解释

dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是

dispatch_semaphore_create,dispatch_semaphore_signal,dispatch_semaphore_wait。

下面我们逐一介绍三个函数:

①dispatch_semaphore_create的声明为:

dispatch_semaphore_t  dispatch_semaphore_create(long value);

传入的参数为long,输出一个dispatch_semaphore_t类型且值为value的信号量。

值得注意的是,这里的传入的参数value必须大于或等于0,否则dispatch_semaphore_create会返回NULL。

(关于信号量,我就不在这里累述了,网上很多介绍这个的。我们这里主要讲一下dispatch_semaphore这三个函数的用法)。

②dispatch_semaphore_signal的声明为:

long dispatch_semaphore_signal(dispatch_semaphore_t dsema)

这个函数会使传入的信号量dsema的值加1;(至于返回值,待会儿再讲)
dispatch_semaphore_signal的返回值为long类型,当返回值为0时表示当前并没有线程等待其处理的信号量,其处理

的信号量的值加1即可。当返回值不为0时,表示其当前有(一个或多个)线程等待其处理的信号量,并且该函数唤醒了一

个等待的线程(当线程有优先级时,唤醒优先级最高的线程;否则随机唤醒)。

dispatch_semaphore_wait的返回值也为long型。当其返回0时表示在timeout之前,该函数所处的线程被成功唤醒。

当其返回不为0时,表示timeout发生。

③dispatch_semaphore_wait的声明为:

long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);

这个函数会使传入的信号量dsema的值减1;

这个函数的作用是这样的,如果dsema信号量的值大于0,该函数所处线程就继续执行下面的语句,并且将信号量的值减1;

如果desema的值为0,那么这个函数就阻塞当前线程等待timeout(注意timeout的类型为dispatch_time_t,

不能直接传入整形或float型数),如果等待的期间desema的值被dispatch_semaphore_signal函数加1了,

且该函数(即dispatch_semaphore_wait)所处线程获得了信号量,那么就继续向下执行并将信号量减1。

如果等待期间没有获取到信号量或者信号量的值一直为0,那么等到timeout时,其所处线程自动执行其后语句。

关于信号量,这里有一个很形象的解释:

1
2
3
4
5
6
7
停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值就相当于剩余车位的数目,dispatch_semaphore_wait函数就相当于来了一辆车,dispatch_semaphore_signal
就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),
调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait剩余车位就减少一个;
当剩余车位为0时,再来车(即调用dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主
没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,
所以就一直等下去。

本文到此就结束了


参考:

Parse源码浅析系列(一)—Parse的底层多线程处理思路:GCD高级用法

关于dispatch_semaphore的使用

GCD使用经验与技巧浅谈

iOS 并发编程之 Operation Queues

关于iOS多线程,你看我就够了

小笨狼漫谈多线程:NSThread