0%

夜的随笔-三

苍耳

  最近可能太忙了吧,或许这都是人长大后的代价,慢慢地,我们从家的羽翼里挣脱,仿佛获得了自由,却在不自知的淡漠里,失去了越来越多的羁绊,因为有些东西,在成人的世界里,或许早已失去了它原本的意义。

  有时候觉得自己明明没什么重要的,但依旧有一堆事情摆在面前。今晚难得看了场电影,徐峥说我才不想当救世主,我只想赚钱。到后来却真的拯救了一些人,也因此受到法律的惩罚。我在剧中所见却尽是小人物的悲凉,竭尽全力想要活下来,也依旧力不从心。

  回来的路上,路灯依旧耀眼。这座并不怎么繁华的城市,也依旧光怪陆离。风钻进车窗,灯却模糊了方向。走在校园的路上,法桐的光影依稀可见,启明楼下的白色灯光也依旧好看,夜风吹呀吹呀,梧桐的叶子,自东而西,俯瞰着整片森林。

  现在很少会静下心来看一本书,写一篇文章了,后来慢慢地,甚至忘记了写字的感觉,那种酣畅淋漓,浑然天成的自得感,如今却都演化成了铿锵的键盘声,血淋淋的时光下,全是单调的梦想。但这些,都是自己的选择,很多东西,或许只有在夜深人静的时候,才会有那种怅然若失的感觉吧,然后或许会感慨几番,或许也会彻彻底底地伤心一会,最后却都一梦即休。走着走着,不自禁便丢了很多东西,我的错,而不是时光。

  有些人也总是要走的,咫尺天涯或是如此吧。有时候会幻想在多年之后我们再次重逢的场景,或许是一家弹着吉他的咖啡店,或许是在人来人往的街角,或许只是相逢一笑,或许只是擦肩默然。那些曾被我珍藏在心底的回忆,或许终将成为我青春过往里,最美好也最单纯的遗憾,然后放入时光的漂流瓶里,随之消逝,杳无回音。

  最近喜欢跟一两朋友出东小门外右拐,喝酒撸串。开始越来越喜欢上喝酒,一般将醉辄止,却不想喝过,因为怕了醉酒的感觉。记得一次喝酒醉了,从一天下午睡到第二天中午,吐了一晚上却没有人会在旁边唠叨照顾,人在脆弱的时候总是会想起曾经温柔的人和地方,而出门在外,所以要尽量收敛自己脆弱的一面,或伪装,或修炼。所以做很多事情,浅尝辄止便好。

  已经写不出很长的文章了,也渐渐没有了风花雪月的雅兴,偶尔诗兴大发,还不及写下一两句,便被琐事缠身,不了了之。明日难得周末,我要睡到自然醒,再说一句,世界晚安。

夜的随笔-一

苍耳

  夜里翻来覆去地怎么也睡不着,恍惚间总是想起许多过去了的事。在生活面前,我只是一个渺小得近乎卑微的人,随着时间的浪潮,在这片汹涌里翻滚。

  忽然之间我想起了家里那一棵老迈的樱桃树,粗糙的枝条全是岁月的鞭痕,那些褶皱边缘的伤痕开始发黑,我想起来小时候自己总是喜欢用手去抓它那些起伏的树枝,那种硌手的感觉如今念起心里竟是充满了一种似曾相识的沧桑,那是我这一生再也无法企及的时光,也是我将渐渐遗失的感觉。

  但我还是记得那些阳光的味道,安静而内敛,让我在这些琐碎平常的日子里,能够拥有广阔的时间和空间,去感受每一分温暖。

  许多喜欢写字的人都会对”流浪”这个词有着一种似与灵魂相契合的衷爱。有句话叫“人生应该要有一场说走就走的旅行”,也曾幻想自己能在一个阳光温热的夏天背着背包行一场流浪,在广袤的世界踩下我的脚印,让我觉得我来过外面的世界,也留下过我的烙印。让这片慵懒的阳光随着我的脚步成为记忆的永恒,让这流金的岁月,拥有一丝不同于往日的涟漪。

  后来我开始讨厌这所谓的流浪了。至少当我因为买不到票而在火车上站了八个小时后我开始对它失了兴趣,或者当我看到姐姐从中途站下了车而我被这叫做离愁的东西搅得不能自已后,我不再对它趋之若鹜。这只是一场别有用心的出行,也许是我从未让自己轻松地旅行过,即使我可以拥有这样一个机会我觉得我也会把自己丢掉,丢在一个别人找不到的地方,然后,只能独自蜷着忍受这份恐惧。总之,我不会喜欢这个称之为”流浪”的词了。

  小的时候就开始喜欢晴天,刚开始的理由只是近视了没敢去戴眼镜而阴天会看不清黑板,而后来则是纯粹的怕冷。许是多年的相濡以沫竟让我和它开始惺惺相惜了起来,总之我就慢慢习惯了喜欢阳光的日子。

  很多的晴天都是安静的日子,只要能从远处默默地看着它我心里就会突然变得很踏实。在亚麻色的午后,一觉醒来可以有一缕缕的阳光透过窗掉到我卧室的墙上,那点点光斑随着屋外竹林的晃动而上下跳着,恍惚间觉得这个世界若永远只是我眼前的宁静该有多好。

  有很多时候当我怔怔望着落在这个世界的阳光之时,我总会产生一种如老人垂暮般的错觉。我的家乡一般都是很安静的。微风拂过花香,吹起一阵麦浪,漫天的野絮在空中飞扬,阳光透过叶隙,洒下一粒粒若隐若现的斑点,门前的狗微闭着眼,似已厌倦眼前这光景……于是在这样一种宁静的环境之下,所有一切都显得很祥和。除了那些能动的生命,其余的,都只拥有着一种老村子的静。正如一个将死之人,只剩下褪却浮沉的睿智与历经沧桑之后的安详。

  因为晴天所以我记忆里的烦恼也是枯黄色的。或许是一些可以称之为自扰的忧虑。忧虑父母为生计的奔波,忧虑家人的身体,忧虑亲人的烦恼。而我,其实什么也做不到。

  我就如此背着枯黄的烦恼,慢慢成长。成长这个词,有着某种血淋淋的残酷,骨感而沧桑。有一次偶然看到了同学女儿的照片,一岁大的婴儿,还很小。我不禁开始觉得是否那些属于我们青春的时代,已经成为了过去?而我们终究是成为了这个社会的一份子。

  岁月就这样离开了。在枯黄的世界,什么也没有留下,夜的尽头我开始看见新的阳光,却依旧是荒无人烟。对于我来说,我想要拥有的,只是那种我向往已久的简单的生活,不是喂马劈柴,只要衣食无忧,只要阖家幸福。这样,便每天都可是晴天。

  每次在自己一个人的时候,我都喜欢发呆,会忍不住去想很多东西,许是想得太多,所以我竟然觉得自己仿佛经历了许多人事变迁的沧桑。我以为我真的成为了一个老人。

  在这些兜兜转转的时光里,我总会有许多个夜晚翻来覆去地失眠,然后总是会去回忆起那些不在了的青春,我时常会给那个自己一次重新开始的机会,好让我在回忆里可以完成那些我明明很喜欢却没有去做的遗憾,或者是在那曾悄悄流走的时间里和某个人去打个招呼,也可能是去和那些珍藏在心里的往日时光做个了断…甚至只是重新来一场平凡的邂逅。我都会觉得自己曾经也很幸福。

  在那些的夜晚,我都会去想起我经历过的事,想起永远陪着我的同桌,永远可以和孩子般开玩笑的朋友……这些,都是岁月所无法重新赋予我的幸福。我其实很贪心。

  晴天的味道总喜欢和回忆纠缠在一起,所有过去了的,痛苦或是幸福,都被岁月赋予了新的感觉,一种让人心疼的老旧照片的味道。我喜欢晴天里的树,喜欢看树下那些随风摇曳的斑点,当它们从地上划过无痕的光线之时,我觉得这不正是我们流逝的时光的轨迹吗?

  我一直都不习惯叫别人外号,但我却一直记得这些我觉得很奇怪的外号。我想念学校的树,我想念家里的阳光。多么希望,有一个下午,当我一觉醒来,阳光还能透过窗洒在我卧室的墙上,门前老树依旧随风飘动,远处会传来孩子的哭声,家人们一如既往地谈论着琐事。这些,都是我一直渴望的幸福。我于现在思念着过往,我呆在回忆里品味这现实的骨感。

  他已离去,遗之回忆。你若安好,还是晴天。

流浪者

苍耳


背离人来人往
我流浪在安静的国度
黑夜将我包围
风吹走我的热情
我在这个夜里
蜷缩着
也不安着

早晨我还要继续流浪
我把树成船
把荷成城
我温暖了受伤的雀鸟
我把太阳当成你
也当成我

我在黄昏的天边
草原上留下我孤独的身影
我坐着
没有人可以说话
我躺着
没有人可以叹息
岁月悄悄爬上我的肩
我累了
埋在了土里

雨中·一

苍耳

下着雨
巷子里满是雨声
知了不知何时失了鸣叫
鸟忘了捕食
竹叶低着头
也许沉思着也许在沉睡
只有雨声弥漫

它乘着风也借着土地的回应
抵达了我平静的梦里
激起一层涟漪
我睁开惺忪的双眼
只有雨声无尽

氤氲的薄烟灌满了眼中的世界
山水若隐若现
雾霭里的山鸟花树更显宁静
朦胧晃影缱绻连绵
满是褶皱的天空像极了老人的脸
偶有夏风拂过
带来了不属于这个季节的微凉
雨声延续
正是大自然的心跳

推开门
换上最幸福的表情
我让自己去雨中旅行
让头发淋湿
让身体沐浴
接着是心
我让左手握着右手
记起了那些放肆的年代

捣练子·醒雨

苍耳

竹箬笠,雨寻忧。
半透窗台半掩秋。
风入小楼轻梦醒,
晚霞还过夜来愁。

捣练子·小寒

苍耳

昨梦醒,雨滴答。
雪降如霜饮午茶。
农事未休风不止,
落梅深处有人家。

清平乐·秋思

苍耳

  异乡别处,秋景无时住。鸿雁轻嘤南北度,惆怅此般环顾。      黄昏却好伤秋,梧桐老叶枯休,独念春花尚早,谁言弯月如勾?。

渔歌子·重九

苍耳

陌上闲枝半过秋,重阳当饮酒难休。
杯入曲,晚归悠。炊烟袅袅正秋收。

谢池春·念春

苍耳

  庭外葳蕤,流水落花春好。兴难收,阑珊趁早。新竹林立,燕归声巧。雀双飞,蝉声微噪。      飞花自在,恨有轻风相扰。岁重重,繁华渐老。一朝风雨,几朝寒晓。雨携情,念天晴少。

基本操作

  • 创建数据库:create database if not exists dbName default character set UTF8;
  • 修改数据库编码:alter database dbName default character set UTF8;
  • 删除数据库:drop database if exists dbName;

备份与还原

  • 备份一个数据库:mysqldump -u username -p dbname table1 table2 …(若没有指定table则备份整个数据库) > backup.sql
  • 备份多个数据库:mysqldump -u username -p –databases dbname2 dbname2 > backup.sql
  • 备份所有数据库:mysqldump -u username -p -all-databases > backup.sql
  • 还原数据:mysql -u root -p [dbname] < backup.sql

backup.sql内容:

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
-- MySQL dump 10.13  Distrib 5.6.30, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: rire
-- ------------------------------------------------------
-- Server version 5.6.30-1

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;

--
-- Table structure for table `contract`
--

DROP TABLE IF EXISTS `contract`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `contract` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`location` varchar(255) NOT NULL,
`order_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `contract_order_id_fk` (`order_id`),
KEY `contract_user_id_fk` (`user_id`),
CONSTRAINT `contract_order_id_fk` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `contract_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `contract`
--

LOCK TABLES `contract` WRITE;
/*!40000 ALTER TABLE `contract` DISABLE KEYS */;
/*!40000 ALTER TABLE `contract` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `order`
--

DROP TABLE IF EXISTS `order`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`time` bigint(20) NOT NULL,
`status` int(11) NOT NULL,
`user_supplier_id` int(11) NOT NULL,
`user_buyer_id` int(11) NOT NULL,
`pro_supplier_id` int(11) NOT NULL,
`pro_buyer_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `order_user_supplier_fk` (`user_supplier_id`),
KEY `order_user_buyer_fk` (`user_buyer_id`),
KEY `order_pro_supplier_fk` (`pro_supplier_id`),
KEY `order_pro_buyer_fk` (`pro_buyer_id`),
CONSTRAINT `order_pro_buyer_fk` FOREIGN KEY (`pro_buyer_id`) REFERENCES `product` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `order_pro_supplier_fk` FOREIGN KEY (`pro_supplier_id`) REFERENCES `product` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `order_user_buyer_fk` FOREIGN KEY (`user_buyer_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `order_user_supplier_fk` FOREIGN KEY (`user_supplier_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `order`
--

LOCK TABLES `order` WRITE;
/*!40000 ALTER TABLE `order` DISABLE KEYS */;
/*!40000 ALTER TABLE `order` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `product`
--

DROP TABLE IF EXISTS `product`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `product` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`pro_type` varchar(255) DEFAULT NULL,
`type` int(11) NOT NULL,
`description` varchar(255) NOT NULL,
`image` varchar(255) DEFAULT NULL,
`time` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `product_user_id_fk` (`user_id`),
CONSTRAINT `product_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `product`
--

LOCK TABLES `product` WRITE;
/*!40000 ALTER TABLE `product` DISABLE KEYS */;
/*!40000 ALTER TABLE `product` ENABLE KEYS */;
UNLOCK TABLES;

--
-- Table structure for table `user`
--

DROP TABLE IF EXISTS `user`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`phone` varchar(20) NOT NULL,
`address` varchar(255) NOT NULL,
`type` int(11) NOT NULL,
`company` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_name_uindex` (`name`),
UNIQUE KEY `user_phone_uindex` (`phone`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;

--
-- Dumping data for table `user`
--

LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;
INSERT INTO `user` VALUES (7,'rire','15927382214','华中科技大学',0,'hhh');
/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2019-04-10 15:32:05

由于原表中存在外键,所以sql文件中的建表顺序会导致还原失败,应该在备份的时候指定备份表的顺序.

数据类型

整型

无符号则在其后加 UNSIGNED

浮点型

日期型

  • year
  • time
  • date
  • datetime
  • timestamp

字符型

联表查询

  • LEFT JOIN 左连接查询: 查询两个表中共有的数据,并以左边的表为基准显示左表的全部数据,显示右表符合条件的数据, 不足的地方显示NULL
  • RIGHT JOIN 右连接查询: 查询两个表共有的数据,并以右表为基准显示右表的全部数据,显示左表符合条件的数据不足的地方显示NULL
  • INNER JOIN 内连接查询: 显示两个表共有的数据
1
2
3
SELECT * FROM tbl_a AS a LEFT JOIN tbl_b AS b ON a.id=b.id;
SELECT * FROM tbl_b AS a RIGHT JOIN tbl_a AS b ON a.id=b.id;
SELECT * FROM tbl_a as a INNER JOIN tbl_b AS b ON a.id=b.id;

约束

NOT NULL

PRIMARY KEY

UNIQUE KEY

FOREIGN KEY

概述

  1. MySQL中“键”和“索引”的定义相同,所以外键和主键一样也是索引的一种。MySQL会自动为所有表的主键进行索引,但是外键字段必须由用户进行明确的索引。用于外键关系的字段必须在所有的参照表中进行明确地索引,InnoDB不能自动地创建索引。
  2. 外键可以是一对一的,一个表的记录只能与另一个表的一条记录连接,或者是一对多的,一个表的记录与另一个表的多条记录连接。
  3. 外键的使用条件
    • 两个表必须是InnoDB表,MyISAM表暂时不支持外键
    • 外键列必须建立了索引,MySQL4.1.2以后的版本在建立外键时会自动创建索引,但如果在较早的版本则需要显式建立;
    • 外键关系的两个表的列必须是数据类型相似,也就是可以相互转换类型的列,比如int和tinyint可以,而int和char则不可以;
  4. 外键的好处:可以使得两张表关联,保证数据的一致性和实现一些级联操作。保持数据一致性,完整性,主要目的是控制存储在外键表中的数据。
  5. 和b表链表后,两表之间建立了链表关系,a表受b表约束,也就是当a表添加或者修改一条数据时、这条数据的外键字段值如果是b表主键字段不存在的,将无法添加,会报错。
  6. 语法:
1
2
3
4
5
6
7
[CONSTRAINT symbol] FOREIGN KEY [id] (index_col_name, ...)
REFERENCES tbl_name (index_col_name, ...)
[ON DELETE {RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT}]
[ON UPDATE {RESTRICT | CASCADE | SET NULL | NO ACTION | SET DEFAULT}]

--该语法可以在 CREATE TABLE 和 ALTER TABLE 时使用,如果不指定CONSTRAINT symbol,MYSQL会自动生成一个名字。
-- ON DELETE、ON UPDATE表示事件触发限制,可设参数.

使用方法

创建表并且设置外键链表

1
2
KEY 外键名称 (外键字段), #设置外键名称
CONSTRAINT 外键名称 FOREIGN KEY (外键字段) REFERENCES 连接表名称 (连接表主键字段) #设置外键链表
1
2
3
4
5
6
7
8
9
10
CREATE TABLE `usr` (
`id` int(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`yhm` char(255) NOT NULL COMMENT '用户名',
`xb` char(255) NOT NULL COMMENT '性别',
`nl` int(255) NOT NULL COMMENT '年龄',
`fzu` int(255) NOT NULL COMMENT '分组',
PRIMARY KEY (`id`),
KEY `usr-usr_fzu` (`fzu`),
CONSTRAINT `usr-usr_fzu` FOREIGN KEY (`fzu`) REFERENCES `usr_fzu` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表-连表usr_fzu';

删除表的外键链表

1
ALTER TABLE 删除外键的表名称 DROP FOREIGN KEY 创建外键时的外键名称;#删除外键
1
ALTER TABLE usr DROP FOREIGN KEY usr_usr_fzu;

创建表的外键链表

1
ALTER TABLE 要创建外键的表 ADD CONSTRAINT 设置外键名称 FOREIGN KEY (外键字段) REFERENCES 要连接的表名称 (要连接表的主键id);#创建外键
1
ALTER TABLE usr ADD CONSTRAINT usr_usr_fzu FOREIGN KEY (fzu) REFERENCES usr_fzu (id);

事件触发

在父表(先创建)上进行update/delete以更新或删除在子表中有一条或多条对应匹配行的候选键时,父表的行为取决于:在定义子表的外键时指定的on update/on delete子句。

关键字 含义
CASCADE 关联操作,如果主表被更新或删除,从表也会执行相应的操作
RESTRICT 拒绝主表的相关操作(这是默认设置,也是最安全的设置)
SET NULL 修改包含与已删除键值有参照关系的所有记录,使用NULL值替换(只能用于已标记为NOT NULL的字段)
SET DEFAULT 设默认值
NO ACTION 无动作

DEFAULT

在创建表的时候添加

  • CREATE TABLE t_user(user_id INT(10) DEFAULT 3);

通过 ALTER 语句

  • ALTER TABLE t_user MODIFY user_id INT(10) DEFAULT 2;
  • ALTER TABLE t_user CHANGE user_id user_id INT(10) DEFAULT 2;

删除默认约束

  • ALTER TABLE t_user MODIFY user_id INT(10);
  • ALTER TABLE t_user CHANGE user_id user_id INT(10);

事务

概述

事务的四个属性(ACID):

  1. 原子性(Atomicity):事务是一个完整的操作。
  2. 一致性(Consistency):当事务完成时,数据必须处于一致状态。在事务开始以前,被操作的数据的完整性处于一致性的状态,事务结束后,被操作的数据的完整性也必须处于一致性状态。拿银行转账来说,一致性要求事务的执行不应改变A、B 两个账户的金额总和。如果没有这种一致性要求,转账过程中就会发生钱无中生有,或者不翼而飞的现象。事务应该把数据库从一个一致性状态转换到另外一个一致性状态。
  3. 隔离性(Isolation):对数据进行修改的所有并发事务是彼此隔离的。
  4. 持久性(Durability):事务完成后,它对于系统的影响是永久性的。

没有隔离性带来的问题:

  • 脏读,最容易理解。另一个事务修改了数据,但尚未提交,而本事务中的SELECT会读到这些未被提交的数据。
  • 不重复读。解决了脏读后,会遇到,同一个事务执行过程中,另外一个事务提交了新数据,因此本事务先后两次读到的数据结果会不一致。
  • 幻读。解决了不重复读,保证了同一个事务里,查询的结果都是事务开始时的状态(一致性)。但是,如果另一个事务同时提交了新数据,本事务再更新时,就会“惊奇的”发现了这些新数据,貌似之前读到的数据是“鬼影”一样的幻觉。
事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

Read Uncommitted(读取未提交内容)

  • 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

Read Committed(读取提交内容)

  • 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。

Repeatable Read(可重读)

  • 这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

Serializable(可串行化)

  • 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

实例

不重复读:

1
2
3
4
5
6
7
8
9
10
11
12
1>begin;
2>select * from user;
>>1,h
2>update user set name='hh' where id=1;
1>select * from user;
>>1,hh
2>update user set name='hhh' where id=1;
1>select * from user;
>>1,hh
1>commit;
1>select * from user;
>>1,hhh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1>begin;
1>select * from user;
>>1,h
2>select * from user;
>>1,h
2>update user set name='hh' where id=1;
1>select * from user;
>>1,h
2>update user set name='hhh' where id=1;
1>select * from user;
>>1,h
1>commit;
1>select * from user;
>>1,hhh
1
2
3
4
5
6
7
8
9
10
11
12
1>begin;
1>select * from user;
>>1,h
2>select * from user;
>>1,h
1>update user set name='hh' where id=1;
1>select * from user;
>>1,hh
2>update user set name='hhh' where id=1;
>>阻塞
1>commit;
2>update 成功

幻读:

会出现两条记录。

MySQL

  • begin
  • commit
  • rollback
  • SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]

SQL优化

  1. 查询条件减少使用函数,避免全表扫描
  2. 减少不必要的表连接
  3. 有些数据操作的业务逻辑可以放到应用层进行实现
  4. 当只要一行数据时使用 LIMIT 1
  5. 为搜索字段建立索引
  6. 复合索引最左原则
  7. 避免select *
  8. 选择正确的存储引擎
  9. 避免隐式类型转换
  10. 不建议使用%前缀模糊查询

概述

MongoDB是一个C++编写的基于分布式文件存储的数据库,是一个介于关系和非关系之间的数据库,当然也属于NoSQL的行列,存储方式和Redis类似,是json格式的kav-value存储方式,只是Redis是内存存储,而MongoDB是和普通的数据库目录一样存储在硬盘上。

配置文件:

dbpath = /home/hearing/Software/mongodb/data/db
logpath = /home/hearing/Software/mongodb/log/mongodb.log
logappend = true
port = 27017

启动:

  • ./bin/mongod -f conf/mongod.conf

NoSQL

NoSQL(Not Only SQL):非关系型数据库。有些数据的数据类型不需要固定的模式,无需多余操作就可以横向扩展。NoSQL无需为事先为要存储的数据建立字段,随时可以存储自定义的数据格式。

  • KV键值对:Redis
  • 文档型数据库:MongoDB
  • 列存储数据库
  • 图关系数据库

分布式数据库的CAP和BASE

概述

  • 一个Redis实例存在16个数据库,默认连接的是0号数据库,使用select num选择连接的数据库。
  • move key db:将某个key移动到某个数据库。
  • ping:返回PONG则连接成功
  • keys pattern
  • del key [key …]
  • exists key [key …]
  • type key
  • dbsize: 查看当前数据库key数量
  • flushdb
  • flushall

数据结构

string

  • set key value [EX seconds] [PX milliseconds] [NX|XX]
  • get key

list

  • 有排序的字符串集合,允许重复元素
  • llen list
  • lpush key value [value …]
  • rpush key value [value …]
  • lrange key start stop (从0开始)
  • lpop key
  • rpop key

set

  • 无排序的字符串集合,不允许重复元素
  • sadd key member [member …]
  • smembers:获取所有元素
  • scard:获取集合中元素个数
  • spop key:随机出栈
  • smove key1 key2 key1中某个值:将key1中某个值移到key2
  • sismember:是否为set中的元素

hash

  • hset key field value
  • hget key field
  • hdel key field [field …]

sorted-set(Zset)

  • zadd key [NX|XX] [CH] [INCR] score member [score member …]

事务

  • multi:开始事务
  • discard:取消事务
  • exec:执行事务
  • watch key [key …]:如果事务开始前这些key被其他命令修改过,那么事务被打断
  • unwatch
  • 正常执行
  • 放弃事务
  • 全体连坐:当某条命令不等执行EXEC便报错,则全部命令放弃执行。
  • 冤头债主:当某条命令在EXEC后才报错,则只会影响它自己的执行。
  • Watch监控:执行了exec或unwatch后所有监控锁都会被清除。

持久化

RDB方式(Redis DataBase)

  • 在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时直接将快照文件恢复到内存。
  • 保存在dump.rdb文件中。
  • 优势:只有一个文件,时间间隔的数据,可以归档为一个文件,方便压缩转移(就一个文件)
  • 劣势:如果宕机,数据损失比较大,因为它是没一个时间段进行持久化操作的。也就是积攒的数据比较多,一旦懵逼,就彻底懵逼了。另外,fork的时候内存中的数据被克隆了一份,大致两倍的膨胀性。
  • 默认打开。

AOF方式(Append only file)

  • 以日志的形式来记录每个写操作,不记录读操作,只能追加文件而不能修改,redis启动之初会根据日志来恢复。
  • 保存在appendonly.aof文件中。
  • 优势:即使宕机,也不会损失数据,也不会破坏原有日志文件,保证数据的安全性。
  • 劣势:文件比rdb大,效率更低。
  • 在redis.conf文件中,默认关闭。

主从复制,读写分离

  • Master以写为主,Slave以读为主。

  • 哨兵模式:

    1. 6379带着6380和6381
    2. conf文件所在目录下新建sentinel.conf文件(名字不能错)。
    3. 配置哨兵:在sentinel文件中填写:sentinel monitor 被监控数据库名字(自己起) 127.0.0.1 6379 1(1表示主机挂掉以后slave投票决定下一个master是谁)
    4. 启动哨兵:Reids-sentinel …/sentinel.conf。
    5. 若哨兵监控到master挂了,则投票使某个slave成为master(假设是6380),当6379重新启动后,它将成为6380的slave。

集群

Jedis

连接

直接连接:

1
2
3
4
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.set("name", "刘家东");
System.out.println(jedis.get("name"));
jedis.close();

连接池方式:

1
2
3
4
5
6
7
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(30);
JedisPool jedisPool = new JedisPool(config, "localhost", 6379);
Jedis jedis = jedisPool.getResource();
jedis.set("name", "Tom");
System.out.println(jedis.get("name"));
jedis.close();

事务

1
2
3
4
Transaction transaction = jedis.multi();
transaction.set("k1", "11");
transaction.set("k2", "22");
transaction.exec();

主从复制

1
2
3
4
5
6
7
Jedis jedis_M = new Jedis("127.0.0.1",6379);
Jedis jedis_S = new Jedis("127.0.0.1",6380);

jedis_S.slaveof("127.0.0.1",6379);

jedis_M.set("class","1122V2");
String result = jedis_S.get("class");

连接池

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
public class JedisPoolUtil {
private static volatile JedisPool jedisPool = null;
private JedisPoolUtil(){}
public static JedisPool getJedisPoolInstance() {
if(null == jedisPool) {
synchronized (JedisPoolUtil.class) {
if(null == jedisPool) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
//配置一个pool可分配多少个jedis实例,如果复制为-1则表示不限制。
poolConfig.setMaxActive(1000);
//控制一个pool最多有多少个状态为空闲的jedis实例
poolConfig.setMaxIdle(32);
//当borrow一个jedis实例时,最大的等待时间
poolConfig.setMaxWait(100*1000);
//获得一个jedis实例时是否检查连接可用,如果为true,则得到的所有jedis实例都是可用的
poolConfig.setTestOnBorrow(true);
jedisPool = new JedisPool(poolConfig,"127.0.0.1",6379);
}
}
}
return jedisPool;
}

public static void release(JedisPool jedisPool,Jedis jedis) {
if(null != jedis) {
jedisPool.returnResourceObject(jedis);
}
}
}

Redisson

概述

Redisson是架设在Redis基础上的一个Java驻内存数据网格。

Jedis和Redisson对比

  • Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;
  • Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。
  • Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
  • Jedis中的方法调用是比较底层的暴露的Redis的API,也即Jedis中的Java方法基本和Redis的API保持着一致,了解Redis的API,也就能熟练的使用Jedis。
  • Redisson中的方法则是进行比较高的抽象,每个方法调用可能进行了一个或多个Redis方法调用。
  • 在分布式开发中,Redisson可提供更便捷的方法。

Stack

1
2
3
4
5
6
1. public Stack() 创建一个空堆栈
2. public boolean empty() 测试堆栈是否为空
3. public E pop() 移除堆栈顶部的对象,并作为此函数的值返回该对象
4. public E push(E item) 把项压入堆栈顶部
5. public E peek() 查看堆栈顶部的对象,但不从堆栈中移除它
6. public boolean empty() 测试堆栈是否为空

Queue

1
2
3
4
5
6
7
8
9
10
1. boolean add() 增加一个元索,如果队列已满,则抛出一个 IIIegaISlabEepeplian 异常
2. boolean offer() 添加一个元素并返回true,如果队列已满,则返回false
3. void put() 添加一个元素,如果队列满,则阻塞

4. E remove() 移除并返回队列头部的元素,如果队列为空,则抛出一个 NoSuchElementException 异常
5. E poll() 移除并返问队列头部的元素,如果队列为空,则返回null
6. E take() 移除并返回队列头部的元素,如果队列为空,则阻塞

7. E element() 返回队列头部的元素,如果队列为空,则抛出一个 NoSuchElementException 异常
8. E peek() 返回队列头部的元素,如果队列为空,则返回null

二叉树

类型

满二叉树

除最后一层无任何子节点外,每一层上的所有结点都有两个子结点,节点数为 2^h-1

完全二叉树

若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。一棵完全二叉树的两棵子树,至少有一棵是满二叉树。

大顶堆/小顶堆

任一结点的值是其子树所有结点的最大值或最小值:

  1. 最大值时,称为“最大堆”,也称大顶堆;
  2. 最小值时,称为“最小堆”,也称小顶堆;

二叉查找树

也称为二叉搜索树、有序二叉树或排序二叉树,是指一棵空树或者具有下列性质的二叉树:

  • 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  • 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  • 任意节点的左、右子树也分别为二叉查找树;
  • 没有键值相等的节点。

二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低。为 O(log n)。

平衡二叉树

空树或者左右两个子树的高度差绝对值不超过1且左右两个子树也都是平衡树。

当在二叉排序树中插入一个节点时,首先检查是否因插入而破坏了平衡,若破坏,则找出其中的最小不平衡二叉树,在保持二叉排序树特性的情况下,调整最小不平衡子树中节点之间的关系,以达到新的平衡。所谓最小不平衡子树,指离插入节点最近且以平衡因子的绝对值大于1的节点作为根的子树。

随着树的高度的增加,动态插入和删除的代价也随之增加。

节点数

普通二叉树

1
2
3
4
public int countNodes(TreeNode root) {
if (root == null) return 0;
return 1 + countNodes(root.left) + countNodes(root.right);
}

时间复杂度: O(N)

满二叉树

1
2
3
4
5
6
7
8
9
10
public int countNodes(TreeNode root) {
int h = 0;
// 计算树的高度
while (root != null) {
root = root.left;
h++;
}
// 节点数为 2^h - 1
return (int) Math.pow(2, h) - 1;
}

时间复杂度: O(logN)

完全二叉树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public int countNodes(TreeNode root) {
TreeNode l = root, r = root;
// 记录左、右子树的高度
int hl = 0, hr = 0;
while (l != null) {
l = l.left;
hl++;
}
while (r != null) {
r = r.right;
hr++;
}
// 如果左右子树的高度相同,则是一棵满二叉树
if (hl == hr) {
return (int)Math.pow(2, hl) - 1;
}
// 如果左右高度不同,则按照普通二叉树的逻辑计算
return 1 + countNodes(root.left) + countNodes(root.right);
}

因为一棵完全二叉树的两棵子树,至少有一棵是满二叉树,因此时间复杂度: O(logN*logN) 而不是 O(N*logN)

遍历

前序
中序
后序

中序遍历

递归算法:中序遍历的顺序为leftTree, root, rightTree,显然先遍历左子树,然后是根,最后右子树。中序遍历的递归算法自然也就出来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
public List<Integer> inOrderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
inorder(root, result);
return result;
}

public void inOrder(TreeNode node, List<Integer> result) {
if (node != null) {
inorder(node.left, result);
result.add(node.val);
inorder(node.right, result);
}
}
  • 时间复杂度: O(n),其中 n 是二叉树的节点数,每一个节点恰好被遍历一次。
  • 空间复杂度: O(n),为递归过程中栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)。

非递归算法:按从左到右进行访问。先指针往左探寻到底,然后观察最后一个非空节点是否有右节点,若有,将该右节点作为新的探寻起点,再进行下一轮的探寻,需要使用stack来帮助缓存之前的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public List<Integer> inOrderTraversal(TreeNode node) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
while (node != null || !stack.empty()) {
if (node != null) {
stack.push(node);
node = node.left;
} else {
node = stack.pop();
result.add(node.val);
node = node.right;
}
}
return result;
}
  • 时间复杂度: O(n),其中 n 是二叉树的节点数,每一个节点恰好被遍历一次。
  • 空间复杂度: O(n),为迭代过程中显式栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)。

前序遍历

递归算法

1
2
3
4
5
6
7
8
9
10
11
12
13
public List<Integer> preOrderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
preOrder(root, result);
return result;
}

public void preOrder(TreeNode node, List<Integer> result) {
if (node != null) {
result.add(node.val);
preOrder(node.left, result);
preOrder(node.right, result);
}
}
  • 时间复杂度: O(n),其中 n 是二叉树的节点数,每一个节点恰好被遍历一次。
  • 空间复杂度: O(n),为递归过程中栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)。

非递归算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public List<Integer> preOrderTraversal(TreeNode node) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
while (node != null || !stack.empty()) {
if (node != null) {
result.add(node.val);
stack.push(node);
node = node.left;
} else {
node = stack.pop();
node = node.right;
}
}
return result;
}
  • 时间复杂度: O(n),其中 n 是二叉树的节点数,每一个节点恰好被遍历一次。
  • 空间复杂度: O(n),为迭代过程中显式栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)。

后序遍历

递归算法

1
2
3
4
5
6
7
8
9
10
11
12
13
public List<Integer> postOrderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
postOrder(root, result);
return result;
}

public void postOrder(TreeNode node, List<Integer> result) {
if (node != null) {
postOrder(node.left, result);
postOrder(node.right, result);
result.add(node.val);
}
}
  • 时间复杂度: O(n),其中 n 是二叉树的节点数,每一个节点恰好被遍历一次。
  • 空间复杂度: O(n),为递归过程中栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)。

非递归算法:和之前中序和前序算法不同,后续遍历的root节点要最后才能被访问,因此,我们若想访问某节点,那么我们需要知道该节点的右节点是否已经被访问过。只有该节点的右节点为null,或者已被访问过,那么该节点才能被访问;否则需要先将右节点访问完。为了判断该节点的右节点是否已经被访问过,需另外设一个记录指针last来指示已经访问过的节点,如果之前访问过的节点last恰为该节点的右节点,说明其右子树已经访问完,应该访问该节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public List<Integer> postOrderTraversal(TreeNode node) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode last = null;
while (node != null || !stack.empty()) {
while (node != null) {
stack.push(node);
node = node.left;
}
node = stack.pop();
if (node.right == null || node.right == last) {
result.add(node.val);
last = node;
node = null;
} else {
stack.push(node);
node = node.right;
}
}
return result;
}
  • 时间复杂度: O(n),其中 n 是二叉树的节点数,每一个节点恰好被遍历一次。
  • 空间复杂度: O(n),为迭代过程中显式栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)。

层序遍历

借助队列实现,且没有递归算法。初始时,根结点入队列;然后 while 循环判断队列不空时,弹出一个结点并访问它,并把它的孩子结点入队列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public List<Integer> levelOrder(TreeNode node) {
List<Integer> result = new ArrayList<>();
if (node == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(node);
while (!queue.isEmpty()) {
node = queue.poll();
if (node != null) {
result.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
}
return result;
}

排序算法

概述

排序算法大体可分为两种:

  • 比较排序,时间复杂度O(nlogn) ~ O(n^2),主要有:冒泡排序,选择排序,插入排序,归并排序,堆排序,快速排序等。
  • 非比较排序,时间复杂度可以达到O(n),主要有:计数排序,基数排序,桶排序等。

排序算法的稳定性:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的.

冒泡排序

  1. 比较相邻的元素,如果前一个比后一个大,就把它们两个调换位置。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个flag来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定

void BubbleSort(int A[], int n)
{
for (int j = 0; j < n - 1; j++) { // 每次最大元素就像气泡一样"浮"到数组的最后
for (int i = 0; i < n - 1 - j; i++) { // 依次比较相邻的两个元素,使较大的那个向后移
if (A[i] > A[i + 1]) { // 如果条件改成A[i] >= A[i + 1],则变为不稳定的排序算法
Swap(A, i, i + 1);
}
}
}
}

鸡尾酒排序(冒泡排序改进)

此算法与冒泡排序的不同处在于从低到高然后从高到低。

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
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- 如果序列在一开始已经大部分排序过的话,会接近O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定

void CocktailSort(int A[], int n) {
int left = 0; // 初始化边界
int right = n - 1;
while (left < right) {
for (int i = left; i < right; i++) { // 前半轮,将最大元素放到后面
if (A[i] > A[i + 1])
{
Swap(A, i, i + 1);
}
}
right--;
for (int i = right; i > left; i--) { // 后半轮,将最小元素放到前面
if (A[i - 1] > A[i]) {
Swap(A, i - 1, i);
}
}
left++;
}
}

选择排序

初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

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
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(n^2)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定

void SelectionSort(int A[], int n)
{
for (int i = 0; i < n - 1; i++) // i为已排序序列的末尾
{
int min = i;
for (int j = i + 1; j < n; j++) // 未排序序列
{
if (A[j] < A[min]) // 找出未排序序列中的最小值
{
min = j;
}
}
if (min != i)
{
Swap(A, min, i); // 放到已排序序列的末尾,该操作很有可能把稳定性打乱,所以选择排序是不稳定的排序算法
}
}
}

插入排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 分类 ------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
// 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定

void InsertionSort(int A[], int n)
{
for (int i = 1; i < n; i++) // 类似抓扑克牌排序
{
int get = A[i]; // 右手抓到一张扑克牌
int j = i - 1; // 拿在左手上的牌总是排序好的
while (j >= 0 && A[j] > get) // 将抓到的牌与手牌从右向左进行比较
{
A[j + 1] = A[j]; // 如果该手牌比抓到的牌大,就将其右移
j--;
}
A[j + 1] = get; // 直到该手牌比抓到的牌小(或二者相等),将抓到的牌插入到该手牌右边(相等元素的相对次序未变,所以插入排序是稳定的)
}
}

二分插入排序(插入排序)

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
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定

void InsertionSortDichotomy(int A[], int n)
{
for (int i = 1; i < n; i++)
{
int get = A[i]; // 右手抓到一张扑克牌
int left = 0; // 拿在左手上的牌总是排序好的,所以可以用二分法
int right = i - 1; // 手牌左右边界进行初始化
while (left <= right) // 采用二分法定位新牌的位置
{
int mid = (left + right) / 2;
if (A[mid] > get)
right = mid - 1;
else
left = mid + 1;
}
for (int j = i - 1; j >= left; j--) // 将欲插入新牌位置右边的牌整体向右移动一个单位
{
A[j + 1] = A[j];
}
A[left] = get; // 将抓到的牌插入手牌
}
}

希尔排序(插入排序)

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
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- 根据步长序列的不同而不同。已知最好的为O(n(logn)^2)
// 最优时间复杂度 ---- O(n)
// 平均时间复杂度 ---- 根据步长序列的不同而不同。
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定

void ShellSort(int A[], int n)
{
int h = 0;
while (h <= n) // 生成初始增量
{
h = 3 * h + 1;
}
while (h >= 1)
{
for (int i = h; i < n; i++)
{
int j = i - h;
int get = A[i];
while (j >= 0 && A[j] > get)
{
A[j + h] = A[j];
j = j - h;
}
A[j + h] = get;
}
h = (h - 1) / 3; // 递减增量
}
}

归并排序

归并排序的实现分为递归实现与非递归(迭代)实现。

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
  4. 重复步骤3直到某一指针到达序列尾
  5. 将另一序列剩下的所有元素直接复制到合并序列尾
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
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(n)
// 稳定性 ------------ 稳定

// 合并两个已排好序的数组[left...mid]和[mid+1...right]
private void merge(int[] arr, int left, int mid, int right) {
int len = right - left + 1;
int i = left; // 前一数组的起始元素
int j = mid + 1; // 后一数组的起始元素
int[] tmp = new int[len];
int index = 0;
while (i <= mid && j <= right) {
tmp[index++] = arr[i] < arr[j] ? arr[i++] : arr[j++];
}
while (i <= mid) {
tmp[index++] = arr[i++];
}
while (j <= right) {
tmp[index++] = arr[j++];
}
for (int k = 0; k < len; k++) {
arr[left++] = tmp[k];
}
}

public void mergeSort(int[] arr, int left, int right) {
if (left == right) {
return; // 当待排序的序列长度为1时,递归开始回溯,进行merge操作
}
int mid = (left + right) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}

堆排序

  1. 由输入的无序数组构造一个最大堆,作为初始的无序区
  2. 把堆顶元素(最大值)和堆尾元素互换
  3. 把堆(无序区)的尺寸缩小1,并调用heapify(A, 0)从新的堆顶元素开始进行堆调整
  4. 重复步骤2,直到堆的尺寸为1
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
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定
public void heapSort(int[] arr) {
int heapSize = buildHeap(arr); // 建立一个最大堆
while (heapSize > 1) { // 堆(无序区)元素个数大于1,未完成排序
// 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
swap(arr, 0, --heapSize);
heapify(arr, 0, heapSize);
}
}

// 建堆,时间复杂度O(n)
private int buildHeap(int[] arr) {
int heapSize = arr.length;
// 从每一个非叶结点开始向下进行堆调整
for (int i = heapSize / 2 - 1; i >= 0; i--) {
heapify(arr, i, heapSize);
}
return heapSize;
}

// arr[i]向下进行堆调整
private void heapify(int[] arr, int i, int heapSize) {
int left = 2 * i + 1; // 左孩子
int right = 2 * i + 2; // 右孩子
int max = i;
if (left < heapSize && arr[left] > arr[max]) {
max = left;
}
if (right < heapSize && arr[right] > arr[max]) {
max = right;
}
if (max != i) {
swap(arr, i, max); // 把当前结点和它的最大(直接)子节点进行交换
heapify(arr, max, heapSize); // 递归调用,继续从当前结点向下进行堆调整
}
}

快速排序

以第一个数字6作为基数,使用双指针i, j进行双向遍历:

  1. i从左往右寻找第一位大于基数(6)的数字,j从右往左寻找第一位小于基数(6)的数字;
  2. 找到后将两个数字进行交换。继续循环交换直到i>=j结束循环;
  3. 最终指针i=j,此时交换基数和i(j)指向的数字即可将数组划分为小于基数(6)/基数(6)/大于基数(6)的三部分,即完成一趟快排;
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
// 分类 ------------ 内部比较排序
// 数据结构 --------- 数组
// 最差时间复杂度 ---- 每次选取的基准都是最大(或最小)的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
// 最优时间复杂度 ---- 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logn次划分就能结束递归,时间复杂度为O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ 主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)
// 稳定性 ---------- 不稳定
public void quickSort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int index = patition(arr, left, right);
quickSort(arr, left, index - 1);
quickSort(arr, index + 1, right);
}

private int patition(int[] arr, int left, int right) {
int key = arr[left]; // 第一个数作为基准
int i = left;
int j = right;
while (i < j) {
while (arr[j] >= key && i < j) {
j--; // 从右往左找第一个小于基数的数
}
while (arr[i] <= key && i < j) {
i++;
}
swap(arr, i, j);
}
swap(arr, left, i);
return i;
}

计数排序

  1. 统计数组A中每个值A[i]出现的次数,存入C[A[i]]
  2. 从前向后,使数组C中的每个值等于其与前一项相加,这样数组C[A[i]]就变成了代表数组A中小于等于A[i]的元素个数
  3. 反向填充目标数组B:将数组元素A[i]放在数组B的第C[A[i]]个位置(下标为C[A[i]] - 1),每放一个元素就将C[A[i]]递减
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
// 分类 ------------ 内部非比较排序
// 数据结构 --------- 数组
// 最差时间复杂度 ---- O(n + k)
// 最优时间复杂度 ---- O(n + k)
// 平均时间复杂度 ---- O(n + k)
// 所需辅助空间 ------ O(n + k)
// 稳定性 ----------- 稳定


const int k = 100; // 基数为100,排序[0,99]内的整数
int C[k]; // 计数数组

void CountingSort(int A[], int n)
{
for (int i = 0; i < k; i++) // 初始化,将数组C中的元素置0(此步骤可省略,整型数组元素默认值为0)
{
C[i] = 0;
}
for (int i = 0; i < n; i++) // 使C[i]保存着等于i的元素个数
{
C[A[i]]++;
}
for (int i = 1; i < k; i++) // 使C[i]保存着小于等于i的元素个数,排序后元素i就放在第C[i]个输出位置上
{
C[i] = C[i] + C[i - 1];
}
int *B = (int *)malloc((n) * sizeof(int));// 分配临时空间,长度为n,用来暂存中间数据
for (int i = n - 1; i >= 0; i--) // 从后向前扫描保证计数排序的稳定性(重复元素相对次序不变)
{
B[--C[A[i]]] = A[i]; // 把每个元素A[i]放到它在输出数组B中的正确位置上
// 当再遇到重复元素时会被放在当前元素的前一个位置上保证计数排序的稳定性
}
for (int i = 0; i < n; i++) // 把临时空间B中的数据拷贝回A
{
A[i] = B[i];
}
free(B); // 释放临时空间
}

基数排序

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
// 分类 ------------- 内部非比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n * dn)
// 最优时间复杂度 ---- O(n * dn)
// 平均时间复杂度 ---- O(n * dn)
// 所需辅助空间 ------ O(n * dn)
// 稳定性 ----------- 稳定

const int dn = 3; // 待排序的元素为三位数及以下
const int k = 10; // 基数为10,每一位的数字都是[0,9]内的整数
int C[k];

int GetDigit(int x, int d) // 获得元素x的第d位数字
{
int radix[] = { 1, 1, 10, 100 };// 最大为三位数,所以这里只要到百位就满足了
return (x / radix[d]) % 10;
}

void CountingSort(int A[], int n, int d)// 依据元素的第d位数字,对A数组进行计数排序
{
for (int i = 0; i < k; i++)
{
C[i] = 0;
}
for (int i = 0; i < n; i++)
{
C[GetDigit(A[i], d)]++;
}
for (int i = 1; i < k; i++)
{
C[i] = C[i] + C[i - 1];
}
int *B = (int*)malloc(n * sizeof(int));
for (int i = n - 1; i >= 0; i--)
{
int dight = GetDigit(A[i], d); // 元素A[i]当前位数字为dight
B[--C[dight]] = A[i]; // 根据当前位数字,把每个元素A[i]放到它在输出数组B中的正确位置上
// 当再遇到当前位数字同为dight的元素时,会将其放在当前元素的前一个位置上保证计数排序的稳定性
}
for (int i = 0; i < n; i++)
{
A[i] = B[i];
}
free(B);
}

void LsdRadixSort(int A[], int n) // 最低位优先基数排序
{
for (int d = 1; d <= dn; d++) // 从低位到高位
CountingSort(A, n, d); // 依据第d位数字对A进行计数排序
}

int main()
{
int A[] = { 20, 90, 64, 289, 998, 365, 852, 123, 789, 456 };// 针对基数排序设计的输入
int n = sizeof(A) / sizeof(int);
LsdRadixSort(A, n);
printf("基数排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}

如果考虑和比较排序进行对照,基数排序的形式复杂度虽然不一定更小,但由于不进行比较,因此其基本操作的代价较小,而且如果适当的选择基数,dn一般不大于log n,所以基数排序一般要快过基于比较的排序,比如快速排序。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序并不是只能用于整数排序。

桶排序

桶排序也叫箱排序。工作的原理是将数组元素映射到有限数量个桶里,利用计数排序可以定位桶的边界,每个桶再各自进行桶内排序(使用其它排序算法或以递归方式继续使用桶排序)。

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
// 分类 ------------- 内部非比较排序
// 数据结构 --------- 数组
// 最差时间复杂度 ---- O(nlogn)或O(n^2),只有一个桶,取决于桶内排序方式
// 最优时间复杂度 ---- O(n),每个元素占一个桶
// 平均时间复杂度 ---- O(n),保证各个桶内元素个数均匀即可
// 所需辅助空间 ------ O(n + bn)
// 稳定性 ----------- 稳定

/* 本程序用数组模拟桶 */
const int bn = 5; // 这里排序[0,49]的元素,使用5个桶就够了,也可以根据输入动态确定桶的数量
int C[bn]; // 计数数组,存放桶的边界信息

void InsertionSort(int A[], int left, int right)
{
for (int i = left + 1; i <= right; i++) // 从第二张牌开始抓,直到最后一张牌
{
int get = A[i];
int j = i - 1;
while (j >= left && A[j] > get)
{
A[j + 1] = A[j];
j--;
}
A[j + 1] = get;
}
}

int MapToBucket(int x)
{
return x / 10; // 映射函数f(x),作用相当于快排中的Partition,把大量数据分割成基本有序的数据块
}

void CountingSort(int A[], int n)
{
for (int i = 0; i < bn; i++)
{
C[i] = 0;
}
for (int i = 0; i < n; i++) // 使C[i]保存着i号桶中元素的个数
{
C[MapToBucket(A[i])]++;
}
for (int i = 1; i < bn; i++) // 定位桶边界:初始时,C[i]-1为i号桶最后一个元素的位置
{
C[i] = C[i] + C[i - 1];
}
int *B = (int *)malloc((n) * sizeof(int));
for (int i = n - 1; i >= 0; i--)// 从后向前扫描保证计数排序的稳定性(重复元素相对次序不变)
{
int b = MapToBucket(A[i]); // 元素A[i]位于b号桶
B[--C[b]] = A[i]; // 把每个元素A[i]放到它在输出数组B中的正确位置上
// 桶的边界被更新:C[b]为b号桶第一个元素的位置
}
for (int i = 0; i < n; i++)
{
A[i] = B[i];
}
free(B);
}

void BucketSort(int A[], int n)
{
CountingSort(A, n); // 利用计数排序确定各个桶的边界(分桶)
for (int i = 0; i < bn; i++) // 对每一个桶中的元素应用插入排序
{
int left = C[i]; // C[i]为i号桶第一个元素的位置
int right = (i == bn - 1 ? n - 1 : C[i + 1] - 1);// C[i+1]-1为i号桶最后一个元素的位置
if (left < right) // 对元素个数大于1的桶进行桶内插入排序
InsertionSort(A, left, right);
}
}

int main()
{
int A[] = { 29, 25, 3, 49, 9, 37, 21, 43 };// 针对桶排序设计的输入
int n = sizeof(A) / sizeof(int);
BucketSort(A, n);
printf("桶排序结果:");
for (int i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}

Arrays.sort()

Java 中ArrayList和Collections的sort方法都是调用了Arrays.sort()方法,对于基础类型的排序,使用了更为高效的快速排序(不稳定).而对于非基础类型,在jdk1.8之前使用了归并排序,后来弃用,使用了TimSort.sort()

1
2
3
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public static <T> void sort(T[] a, int fromIndex, int toIndex,
Comparator<? super T> c) {
if (c == null) {
sort(a, fromIndex, toIndex);
} else {
rangeCheck(a.length, fromIndex, toIndex);
// Android-changed: LegacyMergeSort is no longer supported
// if (LegacyMergeSort.userRequested)
// legacyMergeSort(a, fromIndex, toIndex, c);
// else
TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);
}
}

查找算法

顺序查找

时间复杂度为O(n)。

二分查找

最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n)。折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。

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
//二分查找(折半查找),版本1
int BinarySearch1(int a[], int value, int n)
{
int low, high, mid;
low = 0;
high = n-1;
while(low<=high)
{
mid = (low+high)/2;
if(a[mid]==value)
return mid;
if(a[mid]>value)
high = mid-1;
if(a[mid]<value)
low = mid+1;
}
return -1;
}

//二分查找,递归版本
int BinarySearch2(int a[], int value, int low, int high)
{
int mid = low+(high-low)/2;
if(a[mid]==value)
return mid;
if(a[mid]>value)
return BinarySearch2(a, value, low, mid-1);
if(a[mid]<value)
return BinarySearch2(a, value, mid+1, high);
}

插值查找

查找成功或者失败的时间复杂度均为O(log2(log2n))。

1
2
3
4
5
6
7
8
9
10
11
//插值查找
int InsertionSearch(int a[], int value, int low, int high)
{
int mid = low+(value-a[low])/(a[high]-a[low])*(high-low);
if(a[mid]==value)
return mid;
if(a[mid]>value)
return InsertionSearch(a, value, low, mid-1);
if(a[mid]<value)
return InsertionSearch(a, value, mid+1, high);
}

斐波那契查找

斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)。

斐波那契查找是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。

斐波那契查找与折半查找很相似,他是根据斐波那契序列的特点对有序表进行分割的。他要求开始表中记录的个数为某个斐波那契数小1,及n=F(k)-1;开始将k值与第F(k-1)位置的记录进行比较(及mid=low+F(k-1)-1),比较结果也分为三种

  1. 相等,mid位置的元素即为所求
  2. >,low=mid+1,k-=2;
    • 说明:low=mid+1说明待查找的元素在[mid+1,high]范围内,k-=2 说明范围[mid+1,high]内的元素个数为n-(F(k-1))= F(k)-1-F(k-1)=Fk-F(k-1)-1=F(k-2)-1个,所以可以递归的应用斐波那契查找。
  3. <,high=mid-1,k-=1。
    • 说明:low=mid+1说明待查找的元素在[low,mid-1]范围内,k-=1 说明范围[low,mid-1]内的元素个数为F(k-1)-1个,所以可以递归的应用斐波那契查找。

复杂度分析:最坏情况下,时间复杂度为O(log2n),且其期望复杂度也为O(log2n)。

树表查找

二叉树查找

复杂度分析:

  • 它和二分查找一样,插入和查找的时间复杂度均为O(logn),但是在最坏的情况下仍然会有O(n)的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡

二叉查找树:

  1. 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 任意节点的左、右子树也分别为二叉查找树。

二叉查找树性质:

  • 对二叉查找树进行中序遍历,即可得到有序的数列。

平衡查找树之2-3查找树

平衡查找树之红黑树

B树和B+树

总结

二叉查找树平均查找性能不错,为O(logn),但是最坏情况会退化为O(n)。在二叉查找树的基础上进行优化,可以使用平衡查找树。平衡查找树中的2-3查找树,这种数据结构在插入之后能够进行自平衡操作,从而保证了树的高度在一定的范围内进而能够保证最坏情况下的时间复杂度。但是2-3查找树实现起来比较困难,红黑树是2-3树的一种简单高效的实现,他巧妙地使用颜色标记来替代2-3树中比较难处理的3-node节点问题。红黑树是一种比较高效的平衡查找树,应用非常广泛,很多编程语言的内部实现都或多或少的采用了红黑树。

除此之外,2-3查找树的另一个扩展——B/B+平衡树,在文件系统和数据库系统中有着广泛的应用。

分块查找

算法思想:将n个数据元素”按块有序”划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须”按块有序”;即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……

  1. 先选取各块中的最大关键字构成一个索引表;
  2. 查找分两个部分:先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;然后,在已确定的块中用顺序法进行查找。

哈希查找

算法思想:

  • 如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。

算法流程:

  1. 用给定的哈希函数构造哈希表;
  2. 根据选择的冲突处理方法解决地址冲突;
  3. 在哈希表的基础上执行哈希查找。

复杂度分析:

  • 单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。

算法设计思想

贪心算法,分治算法,动态规划算法,随机划分算法,回溯算法等。

Lru算法

参考 LinkedHashMap 可知,LinkedHashMap 保存了记录的插入顺序,还可以在此基础上再根据访问顺序(get,put)来排序,可以用来实现 Lru 算法。它继承自 HashMap, 查看 HashMap 的 put 方法可知在 putVal 方法最后会调用 afterNodeInsertion 方法:

1
2
3
4
5
6
7
8
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMapEntry<K,V> first;
// 若 removeEldestEntry 返回 true 则删除第一个节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}

因此可以重写 removeEldestEntry() 方法,当 map 里面的元素个数大于缓存最大容量则删除链表的顶端元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LruLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
private int capacity;

LruLinkedHashMap(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}

@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}

深度/广度优先

Spring

  • 应用IOC和AOP机制,降低系统组件之间的耦合度,便于组件的维护扩展和替换。IOC特性可以降低action 和dao之间的关联,利用AOP进行事务管理或处理共通的部分程序已经做好了,其中一个需求发生改变,不需要大面积改变代码。spring相当于一个开发平台,在上面可以整合各种技术。
  • IOC:控制反转,控制权的转移,应用程序本身不负责依赖对象的创建和维护,而是由外部容器负责创建和维护。获得依赖对象的过程被反转了。
  • DI:依赖注入,IOC 的另一种表述。即组件以一些预定义好的方式(如 getter 方法)接受来自容器的注入。
  • Bean生命周期
(1)BeanFactoryPostProcessor的postProcessorBeanFactory()方法:若某个IoC容器内添加了实现了BeanFactoryPostProcessor接口的实现类Bean,那么在该容器中实例化任何其他Bean之前可以回调该Bean中的postPrcessorBeanFactory()方法来对Bean的配置元数据进行更改,比如从XML配置文件中获取到的配置信息。

(2)Bean的实例化:Bean的实例化是使用反射实现的。

(3)Bean属性注入:Bean实例化完成后,利用反射技术实现属性及依赖Bean的注入。

(4)BeanNameAware的setBeanName()方法:如果某个Bean实现了BeanNameAware接口,那么Spring将会将Bean实例的ID传递给setBeanName()方法,在Bean类中新增一个beanName字段,并实现setBeanName()方法。

(5)BeanFactoryAware的setBeanFactory()方法:如果某个Bean实现了BeanFactoryAware接口,那么Spring将会将创建Bean的BeanFactory传递给setBeanFactory()方法,在Bean类中新增了一个beanFactory字段用来保存BeanFactory的值,并实现setBeanFactory()方法。

(6)ApplicationContextAware的setApplicationContext()方法:如果某个Bean实现了ApplicationContextAware接口,那么Spring将会将该Bean所在的上下文环境ApplicationContext传递给setApplicationContext()方法,在Bean类中新增一个ApplicationContext字段用来保存ApplicationContext的值,并实现setApplicationContext()方法。

(7)BeanPostProcessor预初始化方法:如果某个IoC容器中增加的实现BeanPostProcessor接口的实现类Bean,那么在该容器中实例化Bean之后,执行初始化之前会调用BeanPostProcessor中的postProcessBeforeInitialization()方法执行预初始化处理。

(8)InitializingBean的afterPropertiesSet()方法:如果Bean实现了InitializingBean接口,那么Bean在实例化完成后将会执行接口中的afterPropertiesSet()方法来进行初始化。

(9)自定义的inti-method指定的方法:如果配置文件中使用init-method属性指定了初始化方法,那么Bean在实例化完成后将会调用该属性指定的初始化方法进行Bean的初始化。

(10)BeanPostProcessor初始化后方法:如果某个IoC容器中增加的实现BeanPostProcessor接口的实现类Bean,那么在该容器中实例化Bean之后并且完成初始化调用后执行该接口中的postProcessorAfterInitialization()方法进行初始化后处理。

(11)使用Bean:此时有关Bean的所有准备工作均已完成,Bean可以被程序使用了,它们将会一直驻留在应用上下文中,直到该上下文环境被销毁。

(12)DisposableBean的destory()方法:如果Bean实现了DisposableBean接口,Spring将会在Bean实例销毁之前调用该接口的destory()方法,来完成一些销毁之前的处理工作。

(13)自定义的destory-method指定的方法:如果在配置文件中使用destory-method指定了销毁方法,那么在Bean实例销毁之前会调用该指定的方法完成一些销毁之前的处理工作。


1、BeanFactoryPostProcessor接口与BeanPostProcessor接口的作用范围是整个上下文环境中,使用方法是单独新增一个类来实现这些接口,那么在处理其他Bean的某些时刻就会回调响应的接口中的方法。

2、BeanNameAware、BeanFactoryAware、ApplicationContextAware的作用范围的Bean范围,即仅仅对实现了该接口的指定Bean有效,所有其使用方法是在要使用该功能的Bean自己来实现该接口。

3、第8点与第9点所述的两个初始化方法作用是一样的,我们完全可以使用其中的一种即可,一般情况我们使用第9点所述的方式,尽量少的去来Bean中实现某些接口,保持其独立性,低耦合性,尽量不要与Spring代码耦合在一起。第12和第13也是如此。
  • applicationConfig.ml 中使用反射方式在 IOC 容器中创建 Bean。

  • AOP:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。主要功能是:日志记录,性能统计,安全控制,事务处理,异常处理等。

  • AOP实现方法:1.动态代理。2.AspectJ

  • 使用@RequestMapping 注解映射请求的 URL。可以修饰类和方法。

  • @PathVariable 将占位符参数绑定到控制器处理方法的入参中。

  • @RequestParam 映射请求参数

  • @RequestHeader 映射请求头信息

  • POJO 作为处理方法的参数: Spring MVC 会按请求参数名和 POJO 属性名进行自动匹配,支持级联属性。

  • Session和Cookie的区别

    • 在服务器端保存用户信息:在客户端保存用户信息
    • Session中保存的是Object类型:在Cookie中保存的是String类型
    • 随对话的结束而将其存储的数据销毁,重启浏览器Session 会失效,可以利用 Cookie 将 Session 持久化:Cookie可以长期保存在客户端
    • 保存重要的信息:保存不重要的信息
  • ModelAndView

  • 重定向:response.sendRedirect(),本质上相当于两次请求,地址栏的URL会变。

  • 请求转发:request.getRequestDispatch().forward(req, resp),是一次请求,转发后请求对象会保存,地址栏URL不会变。

  • MyBatis正向工程:

    1. 导包
    2. Spring全局配置文件
    3. 写接口,接口不用实现(代理),与sql映射文件中的一句sql对应
    4. 写sql映射文件
    5. 获取使用
  • MyBatis逆向工程

    1. 导包
    2. Spring全局配置文件
    3. 建立数据库表
    4. 逆向工程配置文件
    5. 代码中运行该配置文件,生成接口和sql映射文件
    6. 获取使用
  • MyBatis缓存

    • 一级缓存:SqlSession级别的缓存,一直开启。与数据库同一次会话期间查询到的数据会放在本地缓存中,以后可以直接从缓存取。两次相同查询期间执行了增删改操作,则缓存会失效。
    • 二级缓存(全局缓存):基于namespace级别的缓存,一个namespace对应一个二级缓存。不同namespace(Mapper)对应的缓存放在自己的map中。只有关闭了会话后,数据才会从一级缓存存入二级缓存中。
  • EhCache是进程内的缓存框架,在集群模式下时,各应用服务器之间的缓存都是独立的,因此在不同服务器的进程间会存在缓存不一致的情况。即使EhCache提供了集群环境下的缓存同步策略,但是同步依然需要一定的时间,短暂的缓存不一致依然存在。在一些要求高一致性(任何数据变化都能及时的被查询到)的系统和应用中,需要使用集中式缓存,比如Redis。

  • JPA:JPA全称为Java Persistence API,JPA吸取了目前Java持久化技术的优点,旨在规范、简化Java对象的持久化工作。使用JPA持久化对象,并不是依赖于某一个ORM框架。

  • SpringData Repository
    Repository 接口是 Spring Data 的一个核心接口,它不提供任何方法,开发者需要在自己定义的接口中声明需要的方法.

    public interface Repository<T, ID extends Serializable> { } 

    Spring Data可以让我们只定义接口,只要遵循 Spring Data的规范,就无需写实现类。Repository是一个空接口,即一个标记接口,会被IOC容器识别为一个Repository Bean纳入到IOC容器中,进而可以在该接口中定义满足一定规范的方法。

    基础的 Repository 提供了最基本的数据访问功能,其几个子接口则扩展了一些功能。它们的继承关系如下: 

    • Repository: 仅仅是一个标识,表明任何继承它的均为仓库接口类
    • CrudRepository: 继承 Repository,实现了一组 CRUD 相关的方法 
    • PagingAndSortingRepository: 继承 CrudRepository,实现了一组分页排序相关的方法 
    • JpaRepository: 继承 PagingAndSortingRepository,实现一组 JPA 规范相关的方法 
    • 自定义的 XxxxRepository 需要继承 JpaRepository,这样的 XxxxRepository 接口就具备了通用的数据访问控制层的能力。
    • JpaSpecificationExecutor: 不属于Repository体系,实现一组 JPA Criteria 查询相关的方法 
  • ORM框架

    Object Relational Mapping,对象-关系映射。项目中的业务实体有两种表现形式:对象和关系数据,即在内存中表现为对象,在数据库中表现为关系数据。

    • 为什么需要ORM框架

      因为对象之间可以存在关联和继承关系,但是在数据库中,关系数据无法表达多对多关联和继承关系。(ps:在数据库原理中,会把逻辑上的多对多转换为多个一对关系才能实现)因此,对象和关系(业务实体的两种表现形式)想要映射正确,项目系统一般以中间件的形式,即持久层框架。

  • Hibernate的特点

    • Hibernate通过修改一个“持久化”对象的属性,从而修改数据库表中对应的记录数据
    • 提供线程和进程两个级别的缓存提升应用程序性能
    • 有丰富的映射方式将Java对象之间的关系(POJO)转换为数据库表之间的关系
    • 屏蔽不同数据库实现之间的差异。在Hibernate中只需通过“方言”的形式指定当前使用的数据库,就可以根据底层数据库的实际情况生成适合的SQL语句
    • 非侵入式。Hibernate不要求持久化类实现任何接口或继承任何类,POJO即可
  • Mybaits的特点:

    • 简单易学。没有任何第三方依赖,最简单只需要2个jar包+几个sql映射文件,通过文档和源代码,即可比较完全的掌握它的设计思路和实现
    • 灵活。不会对应用程序或者数据库的现有设计强加任何影响。Sql写在xml里面,便于统一管理和优化。通过sql基本上可以实现我们不使用数据访问框架可以实现的所有功能。
    • 解除sql与程序代码的耦合。通过提供DAL层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。
    • 提供映射标签,支持对象与数据库的ORM字段关系映射
    • 提供对象关系映射标签,支持对象关系组建维护
    • 提供xml标签,支持编写动态sql
  • 比较

    • Hibernate 对数据库提供了较为完整的封装,封装了基本的DAO层操作,有较好的数据库移植性
    • Mybatis 可以进行更细致的SQL优化,查询必要的字段,但是需要维护SQL和查询结果集的映射,而且数据库的移植性较差,针对不同的数据库编写不同的SQL,
    • Spring Data JPA 极大的简化了数据库访问,可以通过命名规范、注解的方式较快的编写SQL。